Skip to content

Commit

Permalink
[ Fix ] active 상태, 드롭다운 잘림 수정 (#318)
Browse files Browse the repository at this point in the history
* feat: 상태 필터링 active 효과 추가

* style: dropdown layout

* chore: format

* feat: 드롭다운 방향 동적으로 설정

* chore: format fix
  • Loading branch information
ptyoiy authored Jan 30, 2025
1 parent 86fc3b6 commit 2fd74bb
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 62 deletions.
89 changes: 56 additions & 33 deletions src/common/component/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { useOutsideClick } from "@/common/hook/useOutsideClick";
import { handleA11yClick } from "@/common/util/dom";
import { camelToKebab } from "@/common/util/string";
import { Slot } from "@radix-ui/react-slot";
import { type ReactNode, useState } from "react";
import {
type ReactNode,
forwardRef,
useImperativeHandle,
useState,
} from "react";
import { defaultButtonStyle } from "./Menu.css";

type MenuProps = {
Expand All @@ -18,48 +23,66 @@ type MenuProps = {
* **부여되는 속성**: `id`, `aria-labelledby`<br><hr>
*/
renderList: ReactNode;

onClick?: () => void;
};

export type MenuRef = {
toggleMenu: () => void;
menuRef: React.RefObject<HTMLElement>;
};
/**
* trigger button과 list에 토글 기능과 필요한 aria속성을 부여해주는 컴포넌트<br>
* **`renderTriggerButton`은 꼭 clsx를 사용해 className을 결합해야 함**
*/
const Menu = ({ label, renderTriggerButton, renderList }: MenuProps) => {
const [showMenu, setShowMenu] = useState(false);
const handleClick = () => setShowMenu(!showMenu);
const handleClose = () => setShowMenu(false);
const ref = useOutsideClick(handleClose);
const triggerId = camelToKebab(`${label}Toggle`);
const menuId = camelToKebab(label);

return (
<div ref={ref}>
<Slot
className={defaultButtonStyle}
id={triggerId}
aria-label={`${showMenu ? "Close" : "Open"} ${menuId}`}
aria-haspopup="true"
aria-expanded={showMenu}
aria-controls={menuId}
onClick={handleClick}
onKeyDown={handleA11yClick(handleClick)}
tabIndex={0}
>
{renderTriggerButton}
</Slot>
const Menu = forwardRef(
({ label, renderTriggerButton, renderList, onClick }: MenuProps, ref) => {
const [showMenu, setShowMenu] = useState(false);
const handleClick = () => setShowMenu(!showMenu);
const handleClose = () => setShowMenu(false);
const outsideClickRef = useOutsideClick(handleClose);
const triggerId = camelToKebab(`${label}Toggle`);
const menuId = camelToKebab(label);

useImperativeHandle(
ref,
() =>
({
toggleMenu: handleClick,
menuRef: outsideClickRef,
}) as MenuRef,
);

{showMenu && (
return (
<div ref={outsideClickRef}>
<Slot
id={menuId}
aria-labelledby={triggerId}
onClick={handleClose}
onKeyDown={handleA11yClick(handleClose)}
className={defaultButtonStyle}
id={triggerId}
aria-label={`${showMenu ? "Close" : "Open"} ${menuId}`}
aria-haspopup="true"
aria-expanded={showMenu}
aria-controls={menuId}
onClick={onClick || handleClick}
onKeyDown={handleA11yClick(handleClick)}
tabIndex={0}
>
{renderList}
{renderTriggerButton}
</Slot>
)}
</div>
);
};

{showMenu && (
<Slot
id={menuId}
aria-labelledby={triggerId}
onClick={handleClose}
onKeyDown={handleA11yClick(handleClose)}
>
{renderList}
</Slot>
)}
</div>
);
},
);

export default Menu;
14 changes: 7 additions & 7 deletions src/shared/hook/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { useEffect, useRef } from "react";

export const useFadeIn = <T extends HTMLElement>(
export const useIntersectionObserver = <T extends HTMLElement>(
callback: IntersectionObserverCallback,
) => {
const imageRef = useRef<T>(null);
const elemntRef = useRef<T>(null);

useEffect(() => {
if (!imageRef.current) return;
if (!elemntRef.current) return;
const observer = new IntersectionObserver(callback, { threshold: 0.1 });

observer.observe(imageRef.current);
observer.observe(elemntRef.current);

return () => {
if (imageRef.current) {
observer.unobserve(imageRef.current);
if (elemntRef.current) {
observer.unobserve(elemntRef.current);
}
};
}, []);

return imageRef;
return elemntRef;
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const RoleDropdownMenu = () => {
</div>
}
renderList={
<Dropdown className={dropdownStyle}>
<Dropdown className={dropdownStyle()}>
{Object.keys(ROLE).map((role) => {
const handleClick = () => handleFilterChange(role as Role);
return (
Expand Down
36 changes: 30 additions & 6 deletions src/view/group/setting/MemberList/constant.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { MemberResponse, Role } from "@/app/api/groups/type";
import { IcnCalendarTable, IcnClose } from "@/asset/svg";
import Dropdown from "@/common/component/Dropdown";
import Menu from "@/common/component/Menu/Menu";
import Menu, { type MenuRef } from "@/common/component/Menu/Menu";
import { handleA11yClick } from "@/common/util/dom";
import { ROLE } from "@/shared/constant/role";
import { useIntersectionObserver } from "@/shared/hook/useIntersectionObserver";
import type { TableDataType } from "@/shared/type/table";
import RoleChip from "@/view/group/setting/MemberList/RoleList/RoleChip";
import {
Expand All @@ -19,6 +20,8 @@ import {
} from "@/view/group/setting/MemberList/index.css";
import SortIcon from "@/view/user/setting/GroupList/SortIcon";
import { dropdownStyle } from "@/view/user/setting/GroupList/StatusDropdownMenu/index.css";
import clsx from "clsx";
import { useRef, useState } from "react";
import { chipWrapper } from "./RoleList/index.css";

export const MEMBER_LIST_COLUMNS: TableDataType<MemberResponse>[] = [
Expand Down Expand Up @@ -74,13 +77,28 @@ export const MEMBER_LIST_COLUMNS: TableDataType<MemberResponse>[] = [
},
{
key: "role",
// Header: () => <RoleDropdownMenu />,
Header: () => "역할",
Cell: (data) => {
const [isIntersection, setIsIntersection] = useState(false);
const [direction, setDirection] = useState<"down" | "up">("down");
const menuRef = useRef<MenuRef>(null);
const ref = useIntersectionObserver<HTMLDivElement>(([entry]) => {
setIsIntersection(entry.isIntersecting);
});
const handleOwnerChange = useChangeOwner();
const patchMutate = usePatchMemberRoleMutation();

const handleClick = (role: Role, memberId: number) => {
const handleButtonClick = () => {
const showedBtns = Array.from(
document.querySelectorAll(".intersection"),
);

const index = showedBtns.findIndex((btn) => btn === ref.current);
setDirection(index >= showedBtns.length - 3 ? "up" : "down");
menuRef.current?.toggleMenu();
};

const handleListClick = (role: Role, memberId: number) => {
if (role === "OWNER") {
handleOwnerChange(data.memberId, data.nickname);
return;
Expand All @@ -91,27 +109,33 @@ export const MEMBER_LIST_COLUMNS: TableDataType<MemberResponse>[] = [
return (
<div className={chipWrapper}>
<Menu
ref={menuRef}
label="role"
onClick={handleButtonClick}
renderTriggerButton={
<div
style={{
display: "flex",
width: "fit-content",
}}
ref={ref}
className={clsx(isIntersection && "intersection")}
>
<RoleChip role={data.role as Role} />
</div>
}
renderList={
<Dropdown className={dropdownStyle}>
<Dropdown className={dropdownStyle({ direction })}>
{Object.keys(ROLE).map((role) => {
if (role === data.role) return;
return (
<li
key={role}
onClick={() => handleClick(role as Role, data.memberId)}
onClick={() =>
handleListClick(role as Role, data.memberId)
}
onKeyDown={handleA11yClick(() =>
handleClick(role as Role, data.memberId),
handleListClick(role as Role, data.memberId),
)}
>
<RoleChip role={role as Role} />
Expand Down
4 changes: 2 additions & 2 deletions src/view/onboarding/Section/FadeInImage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useFadeIn } from "@/shared/hook/useIntersectionObserver";
import { useIntersectionObserver } from "@/shared/hook/useIntersectionObserver";
import type { HTMLAttributes, PropsWithChildren } from "react";
import { fadeInStyle } from "./index.css";

Expand All @@ -9,7 +9,7 @@ interface ImageProps
HTMLAttributes<HTMLDivElement> {}

const FadeInImage = ({ children, ...props }: ImageProps) => {
const imageRef = useFadeIn<HTMLDivElement>(function (
const imageRef = useIntersectionObserver<HTMLDivElement>(function (
this: IntersectionObserver,
[e],
) {
Expand Down
34 changes: 27 additions & 7 deletions src/view/user/setting/GroupList/StatusDropdownMenu/index.css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { theme } from "@/styles/themes.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";

export const triggerButtonStyle = style({
display: "flex",
Expand All @@ -16,17 +17,36 @@ export const arrowDownStyle = style({
height: "1.2rem",
});

export const dropdownStyle = style({
transform: "translate(0, 8px)",

paddingTop: "1.6rem",
paddingBottom: "1.6rem",
":hover": {
opacity: 0.9,
export const dropdownStyle = recipe({
base: {
position: "absolute",
zIndex: theme.zIndex.top,
paddingTop: "1.6rem",
paddingBottom: "1.6rem",
":hover": {
opacity: 0.9,
},
},
variants: {
direction: {
down: {
transform: "translate(0, .8rem)",
},
up: {
transform: "translate(0, -14.2rem)",
},
},
},
defaultVariants: {
direction: "down",
},
});

export const textStyle = style({
...theme.font.Caption3_M_12,
color: theme.color.mg2,
});

export const activeStyle = style({
backgroundColor: theme.color.mg5,
});
19 changes: 13 additions & 6 deletions src/view/user/setting/GroupList/StatusDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { IcnBtnArrowDown } from "@/asset/svg";
import Dropdown from "@/common/component/Dropdown";
import Menu from "@/common/component/Menu/Menu";
import { handleA11yClick } from "@/common/util/dom";
import { useGroupListDispatch } from "@/view/user/setting/GroupList/GroupListTable/hook";
import {
useGroupListDispatch,
useGroupListState,
} from "@/view/user/setting/GroupList/GroupListTable/hook";
import {
activeStyle,
arrowDownStyle,
dropdownStyle,
textStyle,
triggerButtonStyle,
} from "@/view/user/setting/GroupList/StatusDropdownMenu/index.css";
import StatusIcon from "@/view/user/setting/GroupList/StatusIcon";
import clsx from "clsx";

const statusOptions = [
{ label: "inProgress", icon: <StatusIcon status="inProgress" /> },
Expand All @@ -18,6 +23,7 @@ const statusOptions = [
];
const StatusDropdownMenu = () => {
const dispatch = useGroupListDispatch();
const { filterValue } = useGroupListState();
const handleFilterChange = (status: string) => {
dispatch({
type: "SET_FILTER",
Expand All @@ -36,16 +42,17 @@ const StatusDropdownMenu = () => {
</div>
}
renderList={
<Dropdown className={dropdownStyle}>
{statusOptions.map((option) => {
const handleClick = () => handleFilterChange(option.label);
<Dropdown className={dropdownStyle()}>
{statusOptions.map(({ label, icon }) => {
const handleClick = () => handleFilterChange(label);
return (
<li
key={option.label}
key={label}
className={clsx(label === filterValue && activeStyle)}
onClick={handleClick}
onKeyDown={handleA11yClick(handleClick)}
>
{option.icon}
{icon}
</li>
);
})}
Expand Down

0 comments on commit 2fd74bb

Please sign in to comment.