From 3a0ea86f1d114374f77bbe5327f78a679ff1f402 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Thu, 6 Jul 2023 21:07:42 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20Dropdown=20=EC=B6=94=EA=B0=80=20s?= =?UTF-8?q?ize=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dropdown/Dropdown.css.ts | 29 +++++++++++++++++---- src/components/Dropdown/DropdownButton.tsx | 30 +++++++++++++++++----- src/components/Dropdown/DropdownItem.tsx | 14 +++++++--- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/components/Dropdown/Dropdown.css.ts b/src/components/Dropdown/Dropdown.css.ts index 126fef17..f568bd97 100644 --- a/src/components/Dropdown/Dropdown.css.ts +++ b/src/components/Dropdown/Dropdown.css.ts @@ -10,17 +10,15 @@ export const wrapper = style({ }); export const buttonBase = style({ - height: '3.5rem', + width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', - padding: `0 ${space.lg}`, - width: '100%', backgroundColor: color.grey50, - borderRadius: borderRadius.md, cursor: 'pointer', border: `1px solid ${color.grey100}`, color: color.sub, + padding: `0 ${space.lg}`, }); export const buttonVariant = recipe({ @@ -30,8 +28,19 @@ export const buttonVariant = recipe({ borderColor: color.red500, }, }, + size: { + medium: { + height: '3.5rem', + borderRadius: borderRadius.md, + }, + small: { + height: '2.5rem', + borderRadius: borderRadius.xs, + }, + }, }, defaultVariants: { + size: 'medium', isError: false, }, }); @@ -133,7 +142,6 @@ export const item = recipe({ base: { ...fontVariant.label2, margin: '0', - padding: `${space.md} ${space.lg}`, listStyleType: 'none', cursor: 'pointer', display: 'flex', @@ -159,9 +167,18 @@ export const item = recipe({ }, }, }, + size: { + medium: { + padding: `${space.md} ${space.lg}`, + }, + small: { + padding: `${space.md} ${space.md}`, + }, + }, }, defaultVariants: { selected: false, + size: 'small', }, }); @@ -170,4 +187,6 @@ export const checkedIconColor = style({ }); export type MenuVariant = RecipeVariants; +export type ButtonVariant = RecipeVariants; +export type Size = NonNullable['size']; export type Placement = NonNullable['placement']; diff --git a/src/components/Dropdown/DropdownButton.tsx b/src/components/Dropdown/DropdownButton.tsx index 1a1ca5a2..d56a8fd8 100644 --- a/src/components/Dropdown/DropdownButton.tsx +++ b/src/components/Dropdown/DropdownButton.tsx @@ -2,22 +2,38 @@ import cx from 'classnames'; import { ButtonHTMLAttributes } from 'react'; -import { buttonBase, buttonVariant, buttonText } from './Dropdown.css'; +import { buttonBase, buttonVariant, buttonText, type Size } from './Dropdown.css'; import CaretDownIcon from './Icons/CaretDownIcon'; import DropdownToggle from './DropdownToggle'; -/** - * @property {string} placeholder? - 선택된 요소가 없을때 기본적으로 버튼에 표시할 텍스트 - */ - interface Props extends ButtonHTMLAttributes { + /** + * 선택된 요소가 없을때 기본적으로 버튼에 표시할 텍스트 @default 선택 + */ placeholder?: string; + + /** + * 버튼의 크기를 설정한다. @default medium + */ + size?: Size; + + /** + * 에러 상태의 유무 @default false + */ isError?: boolean; } -const DropdownButton = ({ children, placeholder = '선택', onClick, isError, ...rest }: Props) => { +const DropdownButton = ({ + children, + placeholder = '선택', + onClick, + isError = false, + size, + className, + ...rest +}: Props) => { return ( - + {children ?? placeholder} diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index c2aec4aa..fff5c644 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -1,7 +1,7 @@ 'use client'; import { LiHTMLAttributes, MouseEvent } from 'react'; -import { item, checkedIconColor } from './Dropdown.css'; +import { item, checkedIconColor, type Size } from './Dropdown.css'; import { useDropdownContext } from './contexts/DropdownContext'; import clsx from 'classnames'; @@ -12,10 +12,18 @@ import CheckedIcon from './Icons/CheckedIcon'; * @property {string} itemKey - Item 별로 가지는 고유값 (선택 state 값에 사용됨) */ interface Props extends LiHTMLAttributes { + /** + * Item 별로 가지는 고유값 (선택 state 값에 사용됨) + */ itemKey: string; + + /** + * Item의 크기를 설정한다. @default medium + */ + size?: Size; } -const DropdownItem = ({ children, className, onClick, itemKey, ...rest }: Props) => { +const DropdownItem = ({ children, className, onClick, itemKey, size, ...rest }: Props) => { const { closeDropdown } = useDropdownContext(); const { selectable, selectedItemKey, onSelectChange } = useDropdownMenuContext(); @@ -29,7 +37,7 @@ const DropdownItem = ({ children, className, onClick, itemKey, ...rest }: Props) return (
  • Date: Thu, 6 Jul 2023 21:58:27 +0900 Subject: [PATCH 02/16] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dropdown/DropdownMenu.tsx | 92 +++++++++---------- .../Dropdown/contexts/DropdownMenuContext.tsx | 33 ++++--- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/components/Dropdown/DropdownMenu.tsx b/src/components/Dropdown/DropdownMenu.tsx index d9f091b0..21714eac 100644 --- a/src/components/Dropdown/DropdownMenu.tsx +++ b/src/components/Dropdown/DropdownMenu.tsx @@ -7,63 +7,63 @@ import { useDropdownContext } from './contexts/DropdownContext'; import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; import { getComputedPlacement } from './utils/placement'; -/** - * @property {Placement} placement? - Menu를 표시할 기준 위치를 선정하는 값 - */ interface Props extends HTMLAttributes, Partial { - placement?: Placement; + /** + * Menu를 표시할 기준 위치를 선정하는 값 + */ + placement?: Placement; } export interface DropdownMenuHandle { - /** menu를 닫는다 */ - close: () => void; + /** menu를 닫는다 */ + close: () => void; } const DropdownMenu = forwardRef( - ( - { - children, - className, - selectable = false, - selectedItemKey = null, - onSelectChange = () => null, - placement = 'bottom', - ...rest - }, - ref, - ) => { - const { isOpen, closeDropdown, yplacement: systemYPlacement, menuRef, menuId } = useDropdownContext(); + ( + { + children, + className, + selectable = false, + selectedItemKey = null, + onSelectChange = () => null, + placement = 'bottom', + ...rest + }, + ref, + ) => { + const { isOpen, closeDropdown, yplacement: systemYPlacement, menuRef, menuId } = useDropdownContext(); - const menuContextValue = useMemo( - () => ({ selectable, selectedItemKey, onSelectChange }), - [selectable, selectedItemKey, onSelectChange], - ); + const menuContextValue = useMemo( + () => ({ selectable, selectedItemKey, onSelectChange }), + [selectable, selectedItemKey, onSelectChange], + ); - const computedPlacement = useMemo( - () => getComputedPlacement(placement, systemYPlacement), - [placement, systemYPlacement], - ); + const computedPlacement = useMemo( + () => getComputedPlacement(placement, systemYPlacement), + [placement, systemYPlacement], + ); - useImperativeHandle(ref, () => ({ - close: closeDropdown, - })); + useImperativeHandle(ref, () => ({ + close: closeDropdown, + })); - return ( - - - - ); - }, + return ( + + + + ); + }, ); DropdownMenu.displayName = 'DropdownMenu'; diff --git a/src/components/Dropdown/contexts/DropdownMenuContext.tsx b/src/components/Dropdown/contexts/DropdownMenuContext.tsx index ff84cb0c..d39a1c1b 100644 --- a/src/components/Dropdown/contexts/DropdownMenuContext.tsx +++ b/src/components/Dropdown/contexts/DropdownMenuContext.tsx @@ -2,29 +2,34 @@ import { useContext, createContext, ReactNode } from 'react'; -/** - * @property {boolean} selectable - true = 선택 가능모드, false(default) = 선택 불가능모드 - * @property {string | null} selectedItemKey - 선택된 itemkey 값 (selectionMode가 selectable일때만 입력) - * @property {(itemKey: string | null) => void} onSelectChange - 선택 state 업데이트 함수 (selectionMode가 selectable 일때만 입력) - */ - interface DropdownMenuContextValue { - selectable: boolean; - selectedItemKey: string | null; - onSelectChange: (itemKey: string | null) => void; + /** + * true = 선택 가능모드, false(default) = 선택 불가능모드 + */ + selectable: boolean; + + /** + * 선택된 itemkey 값 (selectionMode가 selectable일때만 입력) + */ + selectedItemKey: string | null; + + /** + * 선택 state 업데이트 함수 (selectionMode가 selectable 일때만 입력) + */ + onSelectChange: (itemKey: string | null) => void; } const DropdownMenuContext = createContext(null); const DropdownMenuContextProvider = ({ children, value }: { children: ReactNode; value: DropdownMenuContextValue }) => { - return {children}; + return {children}; }; const useDropdownMenuContext = () => { - const ctx = useContext(DropdownMenuContext); - if (!ctx) - throw new Error('Cannot find DropdownMenuContext. It should be wrapped within DropdownMenuContextProvider.'); - return ctx; + const ctx = useContext(DropdownMenuContext); + if (!ctx) + throw new Error('Cannot find DropdownMenuContext. It should be wrapped within DropdownMenuContextProvider.'); + return ctx; }; export { DropdownMenuContextProvider, useDropdownMenuContext, type DropdownMenuContextValue }; From dd867fefc18137f36b3104b7c677ea37dd24775b Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Thu, 6 Jul 2023 22:36:26 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20DropdownModal=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dropdown/Dropdown.css.ts | 39 ++++++++++--- src/components/Dropdown/DropdownModal.tsx | 68 +++++++++++++++++++++++ src/components/Dropdown/index.ts | 2 + 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/components/Dropdown/DropdownModal.tsx diff --git a/src/components/Dropdown/Dropdown.css.ts b/src/components/Dropdown/Dropdown.css.ts index f568bd97..9215a4c6 100644 --- a/src/components/Dropdown/Dropdown.css.ts +++ b/src/components/Dropdown/Dropdown.css.ts @@ -2,6 +2,7 @@ import { style } from '@vanilla-extract/css'; import { themeTokens } from '@/styles/theme.css'; import { fontVariant } from '@/styles/variant.css'; import { RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { sizeProp } from '@/utils/sizeProp'; const { color, space, borderRadius, zIndices } = themeTokens; @@ -89,13 +90,7 @@ export const menu = recipe({ overflow: 'auto', selectors: { '&::-webkit-scrollbar': { - width: '12px', - }, - '&::-webkit-scrollbar-thumb': { - borderRadius: borderRadius.pill, - border: `4px solid rgba(0,0,0,0)`, - backgroundClip: 'padding-box', - backgroundColor: color.grey300, + display: 'none', }, }, }, @@ -182,6 +177,36 @@ export const item = recipe({ }, }); +export const modal = recipe({ + base: { + padding: space.sm, + position: 'fixed', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + width: 'calc(100% - 40px)', + boxShadow: '0px 7px 14px -7px rgba(0, 0, 0, 0.04), 0px 28px 42px rgba(0, 0, 0, 0.04)', + background: color.white, + borderRadius: borderRadius.sm, + zIndex: zIndices.modal, + maxHeight: sizeProp('540px'), + overflowY: 'scroll', + selectors: { + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + }, + variants: { + open: { + true: {}, + false: { + display: 'none', + }, + }, + }, +}); + export const checkedIconColor = style({ color: color.grey500, }); diff --git a/src/components/Dropdown/DropdownModal.tsx b/src/components/Dropdown/DropdownModal.tsx new file mode 100644 index 00000000..6a27a170 --- /dev/null +++ b/src/components/Dropdown/DropdownModal.tsx @@ -0,0 +1,68 @@ +'use client'; + +import cx from 'classnames'; +import { HTMLAttributes, useMemo, forwardRef, useImperativeHandle } from 'react'; +import { useDropdownContext } from './contexts/DropdownContext'; +import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; +import { modal } from './Dropdown.css'; + +interface Props extends HTMLAttributes, Partial { + variant?: 'centerModal' | 'bottomModal'; +} + +export interface DropdownMenuHandle { + /** menu를 닫는다 */ + close: () => void; +} + +// TODO +// [] variant 처리 (bottomModal, centerModal) + +const DropdownModal = forwardRef( + ( + { + children, + // variant = 'centerModal', + className, + selectable = false, + selectedItemKey = null, + onSelectChange = () => null, + ...rest + }, + ref, + ) => { + const { isOpen, closeDropdown, menuId } = useDropdownContext(); + + const menuContextValue = useMemo( + () => ({ selectable, selectedItemKey, onSelectChange }), + [selectable, selectedItemKey, onSelectChange], + ); + + useImperativeHandle(ref, () => ({ + close: closeDropdown, + })); + + return ( + + + + ); + }, +); + +DropdownModal.displayName = 'DropdownModal'; + +export default DropdownModal; diff --git a/src/components/Dropdown/index.ts b/src/components/Dropdown/index.ts index 07b82fa3..3aa077b6 100644 --- a/src/components/Dropdown/index.ts +++ b/src/components/Dropdown/index.ts @@ -5,12 +5,14 @@ import DropdownButton from './DropdownButton'; import DropdownToggle from './DropdownToggle'; import DropdownItem from './DropdownItem'; import DropdownMenu from './DropdownMenu'; +import DropdownModal from './DropdownModal'; const DropdownRoot = Object.assign(Dropdown, { Button: DropdownButton, Toggle: DropdownToggle, Item: DropdownItem, Menu: DropdownMenu, + Modal: DropdownModal, }); export default DropdownRoot; From eba4945eeb7691d9ac740772a7b64d87169fc9cc Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 12:01:29 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20DropdownModal=20center=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dropdown/Dropdown.css.ts | 42 ++++++++--------------- src/components/Dropdown/DropdownModal.tsx | 30 +++++++--------- src/components/Modal/Modal.css.ts | 2 +- src/components/Modal/Modal.tsx | 5 +-- 4 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/components/Dropdown/Dropdown.css.ts b/src/components/Dropdown/Dropdown.css.ts index 9215a4c6..7fbaa686 100644 --- a/src/components/Dropdown/Dropdown.css.ts +++ b/src/components/Dropdown/Dropdown.css.ts @@ -177,38 +177,24 @@ export const item = recipe({ }, }); -export const modal = recipe({ - base: { - padding: space.sm, - position: 'fixed', - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - width: 'calc(100% - 40px)', - boxShadow: '0px 7px 14px -7px rgba(0, 0, 0, 0.04), 0px 28px 42px rgba(0, 0, 0, 0.04)', - background: color.white, - borderRadius: borderRadius.sm, - zIndex: zIndices.modal, - maxHeight: sizeProp('540px'), - overflowY: 'scroll', - selectors: { - '&::-webkit-scrollbar': { - display: 'none', - }, - }, - }, - variants: { - open: { - true: {}, - false: { - display: 'none', - }, +export const checkedIconColor = style({ + color: color.grey500, +}); + +export const modal = style({ + width: 'calc(100% - 40px) !important', + padding: `${space.sm} !important`, + maxHeight: sizeProp('540px'), + overflow: 'scroll', + selectors: { + '&::-webkit-scrollbar': { + display: 'none', }, }, }); -export const checkedIconColor = style({ - color: color.grey500, +export const modalContent = style({ + margin: '0 !important', }); export type MenuVariant = RecipeVariants; diff --git a/src/components/Dropdown/DropdownModal.tsx b/src/components/Dropdown/DropdownModal.tsx index 6a27a170..bfde6a4f 100644 --- a/src/components/Dropdown/DropdownModal.tsx +++ b/src/components/Dropdown/DropdownModal.tsx @@ -2,11 +2,12 @@ import cx from 'classnames'; import { HTMLAttributes, useMemo, forwardRef, useImperativeHandle } from 'react'; +import { Modal } from '@/components'; import { useDropdownContext } from './contexts/DropdownContext'; import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; -import { modal } from './Dropdown.css'; +import { modal, modalContent } from './Dropdown.css'; -interface Props extends HTMLAttributes, Partial { +interface Props extends HTMLAttributes, Partial { variant?: 'centerModal' | 'bottomModal'; } @@ -16,7 +17,7 @@ export interface DropdownMenuHandle { } // TODO -// [] variant 처리 (bottomModal, centerModal) +// [] bottomsheet랑 같이 넣어보기 const DropdownModal = forwardRef( ( @@ -31,7 +32,7 @@ const DropdownModal = forwardRef( }, ref, ) => { - const { isOpen, closeDropdown, menuId } = useDropdownContext(); + const { isOpen, closeDropdown } = useDropdownContext(); const menuContextValue = useMemo( () => ({ selectable, selectedItemKey, onSelectChange }), @@ -44,20 +45,13 @@ const DropdownModal = forwardRef( return ( - + {isOpen && ( + + + {children} + + + )} ); }, diff --git a/src/components/Modal/Modal.css.ts b/src/components/Modal/Modal.css.ts index ec523306..fa8c771d 100644 --- a/src/components/Modal/Modal.css.ts +++ b/src/components/Modal/Modal.css.ts @@ -7,7 +7,7 @@ const { color, borderRadius, zIndices } = themeTokens; export const modalWrapper = recipe({ base: { - position: 'absolute', + position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 813c6f36..907190fe 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -8,9 +8,10 @@ type ModalProps = { size?: Size; overflow?: boolean; onClose: () => void; + withBackground?: boolean; } & HTMLAttributes; -const Modal = ({ size, onClose, overflow, className, children, ...rest }: ModalProps) => { +const Modal = ({ size, onClose, overflow, className, children, withBackground = true, ...rest }: ModalProps) => { useLockScroll(); const ref = useClickOutside({ onClickOutside: () => { @@ -19,7 +20,7 @@ const Modal = ({ size, onClose, overflow, className, children, ...rest }: ModalP }); return ( <> -
    + {withBackground &&
    }
    {children}
    From 99d0a3abc4998bcef5677dd70bdc7266137b3a1b Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 16:01:28 +0900 Subject: [PATCH 05/16] =?UTF-8?q?chore=20:=20closeDropdown=EC=9D=84=20drop?= =?UTF-8?q?downContext=20=EC=97=90=EC=84=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.json | 1 - src/components/Dropdown/DropdownItem.tsx | 7 +----- src/components/Dropdown/DropdownMenu.tsx | 4 ++-- src/components/Dropdown/DropdownModal.tsx | 24 ++++--------------- .../Dropdown/contexts/DropdownMenuContext.tsx | 5 ++++ 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 72b65fa4..ca0c469e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,6 @@ "@typescript-eslint", "prettier", "react", - "react-hooks", "simple-import-sort", "testing-library", "jest-dom", diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index fff5c644..9ecba844 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -2,15 +2,11 @@ import { LiHTMLAttributes, MouseEvent } from 'react'; import { item, checkedIconColor, type Size } from './Dropdown.css'; -import { useDropdownContext } from './contexts/DropdownContext'; import clsx from 'classnames'; import { useDropdownMenuContext } from './contexts/DropdownMenuContext'; import CheckedIcon from './Icons/CheckedIcon'; -/** - * @property {string} itemKey - Item 별로 가지는 고유값 (선택 state 값에 사용됨) - */ interface Props extends LiHTMLAttributes { /** * Item 별로 가지는 고유값 (선택 state 값에 사용됨) @@ -24,8 +20,7 @@ interface Props extends LiHTMLAttributes { } const DropdownItem = ({ children, className, onClick, itemKey, size, ...rest }: Props) => { - const { closeDropdown } = useDropdownContext(); - const { selectable, selectedItemKey, onSelectChange } = useDropdownMenuContext(); + const { selectable, selectedItemKey, onSelectChange, closeDropdown } = useDropdownMenuContext(); const handleClickItem = (e: MouseEvent) => { if (selectable) onSelectChange(selectedItemKey === itemKey ? null : itemKey); diff --git a/src/components/Dropdown/DropdownMenu.tsx b/src/components/Dropdown/DropdownMenu.tsx index 21714eac..99ddc537 100644 --- a/src/components/Dropdown/DropdownMenu.tsx +++ b/src/components/Dropdown/DropdownMenu.tsx @@ -35,8 +35,8 @@ const DropdownMenu = forwardRef( const { isOpen, closeDropdown, yplacement: systemYPlacement, menuRef, menuId } = useDropdownContext(); const menuContextValue = useMemo( - () => ({ selectable, selectedItemKey, onSelectChange }), - [selectable, selectedItemKey, onSelectChange], + () => ({ selectable, selectedItemKey, onSelectChange, closeDropdown }), + [selectable, selectedItemKey, onSelectChange, closeDropdown], ); const computedPlacement = useMemo( diff --git a/src/components/Dropdown/DropdownModal.tsx b/src/components/Dropdown/DropdownModal.tsx index bfde6a4f..4b8b6671 100644 --- a/src/components/Dropdown/DropdownModal.tsx +++ b/src/components/Dropdown/DropdownModal.tsx @@ -7,36 +7,20 @@ import { useDropdownContext } from './contexts/DropdownContext'; import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; import { modal, modalContent } from './Dropdown.css'; -interface Props extends HTMLAttributes, Partial { - variant?: 'centerModal' | 'bottomModal'; -} +interface Props extends HTMLAttributes, Partial {} export interface DropdownMenuHandle { /** menu를 닫는다 */ close: () => void; } -// TODO -// [] bottomsheet랑 같이 넣어보기 - const DropdownModal = forwardRef( - ( - { - children, - // variant = 'centerModal', - className, - selectable = false, - selectedItemKey = null, - onSelectChange = () => null, - ...rest - }, - ref, - ) => { + ({ children, className, selectable = false, selectedItemKey = null, onSelectChange = () => null, ...rest }, ref) => { const { isOpen, closeDropdown } = useDropdownContext(); const menuContextValue = useMemo( - () => ({ selectable, selectedItemKey, onSelectChange }), - [selectable, selectedItemKey, onSelectChange], + () => ({ selectable, selectedItemKey, onSelectChange, closeDropdown }), + [selectable, selectedItemKey, onSelectChange, closeDropdown], ); useImperativeHandle(ref, () => ({ diff --git a/src/components/Dropdown/contexts/DropdownMenuContext.tsx b/src/components/Dropdown/contexts/DropdownMenuContext.tsx index d39a1c1b..851adcea 100644 --- a/src/components/Dropdown/contexts/DropdownMenuContext.tsx +++ b/src/components/Dropdown/contexts/DropdownMenuContext.tsx @@ -17,6 +17,11 @@ interface DropdownMenuContextValue { * 선택 state 업데이트 함수 (selectionMode가 selectable 일때만 입력) */ onSelectChange: (itemKey: string | null) => void; + + /** + * dropdown menu를 닫는 함수 + */ + closeDropdown: () => void; } const DropdownMenuContext = createContext(null); From ed2e514019115c4e97740c75f5cc44c821cb0683 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 16:26:21 +0900 Subject: [PATCH 06/16] =?UTF-8?q?chore:=20BottomSheet,=20BottomsSheetView?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/BottomSheet.css.ts | 1 + src/components/BottomSheet/BottomSheet.tsx | 27 ++++---------- .../BottomSheet/BottomSheetView.tsx | 36 +++++++++++++++++++ 3 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 src/components/BottomSheet/BottomSheetView.tsx diff --git a/src/components/BottomSheet/BottomSheet.css.ts b/src/components/BottomSheet/BottomSheet.css.ts index 3e21394b..01638efd 100644 --- a/src/components/BottomSheet/BottomSheet.css.ts +++ b/src/components/BottomSheet/BottomSheet.css.ts @@ -46,6 +46,7 @@ export const wrap = recipe({ position: 'fixed', display: 'flex', bottom: '0', + left: '0', width: '100%', background: color.white, zIndex: zIndices.modal, diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx index cfa6f1cd..c4a60173 100644 --- a/src/components/BottomSheet/BottomSheet.tsx +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -1,32 +1,17 @@ 'use client'; -import { HTMLAttributes } from 'react'; -import cx from 'classnames'; -import { titleWrapper, background, wrap } from './BottomSheet.css'; -import IconButton from '../IconButton/IconButton'; + +import { ComponentProps } from 'react'; +import BottomSheetView from './BottomSheetView'; import { useLockScroll } from '@/hooks'; import { useModalControl } from './hooks'; -type BottomSheetProps = { - onClose: () => void; - title?: string; -} & HTMLAttributes; +type BottomSheetProps = Omit, 'isOpen'>; -const BottomSheet = ({ className, onClose, title, children, ...rest }: BottomSheetProps) => { +const BottomSheet = ({ title, onClose, ...rest }: BottomSheetProps) => { useLockScroll(); const { ref, isOpen, closeModalWithTransition } = useModalControl({ onClose }); - return ( - <> -
    -
    -
    -
    {title}
    - -
    - {children} -
    - - ); + return ; }; export default BottomSheet; diff --git a/src/components/BottomSheet/BottomSheetView.tsx b/src/components/BottomSheet/BottomSheetView.tsx new file mode 100644 index 00000000..c22b53e1 --- /dev/null +++ b/src/components/BottomSheet/BottomSheetView.tsx @@ -0,0 +1,36 @@ +import { HTMLAttributes, forwardRef } from 'react'; +import cx from 'classnames'; +import { IconButton } from '@/components'; +import { titleWrapper, background, wrap } from './BottomSheet.css'; + +interface Props extends HTMLAttributes { + /** BottomSheet 제목으로 표시할 텍스트 */ + title?: string; + + /** bottomsheet 열림/닫힘 상태 */ + isOpen: boolean; + + /** bottomsheet 닫을때 호출되는 함수 */ + onClose: () => void; +} + +const BottomSheetView = forwardRef( + ({ title, isOpen, children, onClose, className, ...rest }, ref) => { + return ( + <> +
    +
    +
    +
    {title}
    + +
    + {children} +
    + + ); + }, +); + +BottomSheetView.displayName = 'BottomSheetView'; + +export default BottomSheetView; From dc8e099f44bfdccdd2219b023a6d24d9fea2175c Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 16:27:03 +0900 Subject: [PATCH 07/16] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentSection/ContentSection.tsx | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/app/board/components/ContentSection/ContentSection.tsx b/src/app/board/components/ContentSection/ContentSection.tsx index 6ed542b6..1137bdb2 100644 --- a/src/app/board/components/ContentSection/ContentSection.tsx +++ b/src/app/board/components/ContentSection/ContentSection.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { IconButton, Typography } from '@/components'; +import { IconButton, Typography, Dropdown } from '@/components'; import { getBoardStatusText } from '@/app/board/utils'; import { BoardDetail } from '@/api/types'; @@ -92,7 +92,66 @@ const ContentSection = ({ board }: Props) => {
  • )} -
    {content}
    +
    + {content} + + + + + 서울 + + + 경기도 + + + 강원도 + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + 12 + + + 13 + + + 14 + + + + {/* 바텀시트 내부에 위치한 드랍다운 테스트 해보기 */} +
    From b29f0d2427e6ff153f403e32b8d0f8afed35d07a Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 16:37:04 +0900 Subject: [PATCH 08/16] =?UTF-8?q?chore:=20useModalControl=20hook=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/BottomSheet.tsx | 3 +-- src/components/BottomSheet/hooks/index.ts | 1 - src/hooks/index.ts | 1 + .../BottomSheet => }/hooks/useModalControl/useModalControl.ts | 0 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 src/components/BottomSheet/hooks/index.ts rename src/{components/BottomSheet => }/hooks/useModalControl/useModalControl.ts (100%) diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx index c4a60173..21380c91 100644 --- a/src/components/BottomSheet/BottomSheet.tsx +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -2,8 +2,7 @@ import { ComponentProps } from 'react'; import BottomSheetView from './BottomSheetView'; -import { useLockScroll } from '@/hooks'; -import { useModalControl } from './hooks'; +import { useLockScroll, useModalControl } from '@/hooks'; type BottomSheetProps = Omit, 'isOpen'>; diff --git a/src/components/BottomSheet/hooks/index.ts b/src/components/BottomSheet/hooks/index.ts deleted file mode 100644 index bcd02033..00000000 --- a/src/components/BottomSheet/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useModalControl } from './useModalControl/useModalControl'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 1c711d93..10bf5482 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -16,3 +16,4 @@ export { default as useLockScroll } from './useLockScroll/useLockScroll'; export { default as useLoginRedirect } from './useLoginRedirect/useLoginRedirect'; +export { default as useModalControl } from './useModalControl/useModalControl'; diff --git a/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts b/src/hooks/useModalControl/useModalControl.ts similarity index 100% rename from src/components/BottomSheet/hooks/useModalControl/useModalControl.ts rename to src/hooks/useModalControl/useModalControl.ts From a8e139a2e9caad01bba6ab64309e734eb7407b80 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 17:44:19 +0900 Subject: [PATCH 09/16] =?UTF-8?q?chore:=20hook=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=9B=90=EC=83=81=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/hooks/index.ts | 1 + .../BottomSheet}/hooks/useModalControl/useModalControl.ts | 0 src/hooks/index.ts | 2 -- 3 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 src/components/BottomSheet/hooks/index.ts rename src/{ => components/BottomSheet}/hooks/useModalControl/useModalControl.ts (100%) diff --git a/src/components/BottomSheet/hooks/index.ts b/src/components/BottomSheet/hooks/index.ts new file mode 100644 index 00000000..bcd02033 --- /dev/null +++ b/src/components/BottomSheet/hooks/index.ts @@ -0,0 +1 @@ +export { default as useModalControl } from './useModalControl/useModalControl'; diff --git a/src/hooks/useModalControl/useModalControl.ts b/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts similarity index 100% rename from src/hooks/useModalControl/useModalControl.ts rename to src/components/BottomSheet/hooks/useModalControl/useModalControl.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 10bf5482..1024f806 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -15,5 +15,3 @@ export { default as useClipBoard } from './useClipBoard/useClipBoard'; export { default as useLockScroll } from './useLockScroll/useLockScroll'; export { default as useLoginRedirect } from './useLoginRedirect/useLoginRedirect'; - -export { default as useModalControl } from './useModalControl/useModalControl'; From f5488f60954326eedbdb20d008c86ffd28bad69c Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 17:54:55 +0900 Subject: [PATCH 10/16] =?UTF-8?q?chore:=20BottomSheet=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/BottomSheet.css.ts | 4 +++- src/components/BottomSheet/BottomSheet.tsx | 3 +-- src/components/BottomSheet/BottomSheetView.tsx | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/BottomSheet/BottomSheet.css.ts b/src/components/BottomSheet/BottomSheet.css.ts index 01638efd..b4ea4856 100644 --- a/src/components/BottomSheet/BottomSheet.css.ts +++ b/src/components/BottomSheet/BottomSheet.css.ts @@ -2,6 +2,7 @@ import { themeTokens } from '@/styles/theme.css'; import { style, globalStyle } from '@vanilla-extract/css'; import { fontVariant } from '@/styles/variant.css'; import { recipe } from '@vanilla-extract/recipes'; +import { sizeProp } from '@/utils/sizeProp'; const { color, zIndices } = themeTokens; export const titleWrapper = style({ @@ -53,7 +54,8 @@ export const wrap = recipe({ padding: '20px', borderRadius: '14px 14px 0px 0px', flexDirection: 'column', - justifyContent: 'flex-end', + justifyContent: 'flex-start', + minHeight: sizeProp('632px'), }, variants: { open: { diff --git a/src/components/BottomSheet/BottomSheet.tsx b/src/components/BottomSheet/BottomSheet.tsx index 21380c91..9c56eecf 100644 --- a/src/components/BottomSheet/BottomSheet.tsx +++ b/src/components/BottomSheet/BottomSheet.tsx @@ -2,12 +2,11 @@ import { ComponentProps } from 'react'; import BottomSheetView from './BottomSheetView'; -import { useLockScroll, useModalControl } from '@/hooks'; +import { useModalControl } from './hooks'; type BottomSheetProps = Omit, 'isOpen'>; const BottomSheet = ({ title, onClose, ...rest }: BottomSheetProps) => { - useLockScroll(); const { ref, isOpen, closeModalWithTransition } = useModalControl({ onClose }); return ; diff --git a/src/components/BottomSheet/BottomSheetView.tsx b/src/components/BottomSheet/BottomSheetView.tsx index c22b53e1..c9c888de 100644 --- a/src/components/BottomSheet/BottomSheetView.tsx +++ b/src/components/BottomSheet/BottomSheetView.tsx @@ -1,6 +1,7 @@ import { HTMLAttributes, forwardRef } from 'react'; import cx from 'classnames'; import { IconButton } from '@/components'; +import { useLockScroll } from '@/hooks'; import { titleWrapper, background, wrap } from './BottomSheet.css'; interface Props extends HTMLAttributes { @@ -16,6 +17,8 @@ interface Props extends HTMLAttributes { const BottomSheetView = forwardRef( ({ title, isOpen, children, onClose, className, ...rest }, ref) => { + useLockScroll(); + return ( <>
    From e1a2c6b1c2b9ce18e82e0dc029c6ad20c9974b05 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 17:59:13 +0900 Subject: [PATCH 11/16] =?UTF-8?q?feat:=20DropdownBottomSheet=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dropdown/DropdownBottomSheet.tsx | 56 ++++++++++++++++++ src/components/Dropdown/README.md | 48 +++++++++++++++ .../useDropdownBottomSheet.tsx | 58 +++++++++++++++++++ src/components/Dropdown/index.ts | 2 + 4 files changed, 164 insertions(+) create mode 100644 src/components/Dropdown/DropdownBottomSheet.tsx create mode 100644 src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx diff --git a/src/components/Dropdown/DropdownBottomSheet.tsx b/src/components/Dropdown/DropdownBottomSheet.tsx new file mode 100644 index 00000000..99d31b6b --- /dev/null +++ b/src/components/Dropdown/DropdownBottomSheet.tsx @@ -0,0 +1,56 @@ +'use client'; + +import cx from 'classnames'; +import BottomSheetView from '@/components/BottomSheet/BottomSheetView'; +import { HTMLAttributes, useMemo, forwardRef, useImperativeHandle, ComponentProps } from 'react'; +import { useDropdownContext } from './contexts/DropdownContext'; +import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; +import useDropdownBottomSheet from './hooks/useDropdownBottomSheet/useDropdownBottomSheet'; + +interface Props + extends HTMLAttributes, + Partial, + Omit, 'isOpen' | 'onClose'> {} + +export interface DropdownMenuHandle { + /** menu를 닫는다 */ + close: () => void; +} + +const DropdownBottomSheet = forwardRef( + ({ children, className, selectable = false, selectedItemKey = null, onSelectChange = () => null, ...rest }, ref) => { + const { isOpen: isDropdownOpen, closeDropdown } = useDropdownContext(); + + const [bottomSheetRef, isBottmSheetOpen, , closeBottomSheet] = useDropdownBottomSheet({ + isDropdownOpen, + onClose: closeDropdown, + }); + + const menuContextValue = useMemo( + () => ({ selectable, selectedItemKey, onSelectChange, closeDropdown: closeBottomSheet }), + [selectable, selectedItemKey, onSelectChange, closeBottomSheet], + ); + + useImperativeHandle(ref, () => ({ + close: closeBottomSheet, + })); + + return ( + + + {children} + + + ); + }, +); + +DropdownBottomSheet.displayName = 'DropdownBottomSheet'; + +export default DropdownBottomSheet; diff --git a/src/components/Dropdown/README.md b/src/components/Dropdown/README.md index 6816732e..31840c8e 100644 --- a/src/components/Dropdown/README.md +++ b/src/components/Dropdown/README.md @@ -296,6 +296,54 @@ const [selection, setSelection] = useState(null); - 각 요소들은 커스터마이징 하여 스타일을 조절할 수 있습니다.(className이나 인라인 스타일 이용가능) +### Dropdown.Modal + +```tsx +`Dropdown` 버튼을 통해 모달(모바일에서 화면 가운데 위치한 모달)을 트리거할때 사용합니다. + +const [selection, setSelection] = useState(null); + + + + + + 서울 + + + 경기도 + + + 강원도 + + + +``` + + +### Dropdown.BottomSheet + +`Dropdown` 버튼을 통해 BottomSheet를 트리거할때 사용합니다. + +```tsx +const [selection, setSelection] = useState(null); + + + + + + 서울 + + + 경기도 + + + 강원도 + + + +``` + + ## 참고 사항 - [ ] 향후 sprite icon을 만들면 아이콘 접근 방법을 변경해야할듯 합니다. diff --git a/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx new file mode 100644 index 00000000..3eddf1cd --- /dev/null +++ b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { useClickOutside, useBooleanState } from '@/hooks'; + +interface Props { + isDropdownOpen: boolean; + onClose: () => void; +} + +const useDropdownBottomSheet = ({ isDropdownOpen, onClose }: Props) => { + const [isOpen, setOpen, setClose] = useBooleanState(false); + const closeStatus = useRef(false); + + const openBottomSheet = useCallback(() => { + closeStatus.current = false; + setOpen(); + }, [setOpen]); + + const closeBottomSheet = useCallback(() => { + closeStatus.current = true; + setClose(); + }, [setClose]); + + const ref = useClickOutside({ + onClickOutside: () => { + closeBottomSheet(); + }, + }); + + const handleTransitionEnd = useCallback( + (event: TransitionEvent) => { + if (event.propertyName === 'transform' && closeStatus.current) { + onClose(); + } + }, + [onClose], + ); + + useEffect(() => { + if (isDropdownOpen) { + openBottomSheet(); + } else { + closeBottomSheet(); + } + }, [isDropdownOpen, openBottomSheet, closeBottomSheet]); + + useEffect(() => { + const element = ref.current; + if (!element) return; + element.addEventListener('transitionend', handleTransitionEnd); + return () => { + element.removeEventListener('transitionend', handleTransitionEnd); + }; + }, [ref, handleTransitionEnd]); + + return [ref, isOpen, openBottomSheet, closeBottomSheet] as const; +}; + +export default useDropdownBottomSheet; diff --git a/src/components/Dropdown/index.ts b/src/components/Dropdown/index.ts index 3aa077b6..86764cb5 100644 --- a/src/components/Dropdown/index.ts +++ b/src/components/Dropdown/index.ts @@ -6,6 +6,7 @@ import DropdownToggle from './DropdownToggle'; import DropdownItem from './DropdownItem'; import DropdownMenu from './DropdownMenu'; import DropdownModal from './DropdownModal'; +import DropdownBottomSheet from './DropdownBottomSheet'; const DropdownRoot = Object.assign(Dropdown, { Button: DropdownButton, @@ -13,6 +14,7 @@ const DropdownRoot = Object.assign(Dropdown, { Item: DropdownItem, Menu: DropdownMenu, Modal: DropdownModal, + BottomSheet: DropdownBottomSheet, }); export default DropdownRoot; From c0a191e549c32cf7d26936a4851e493d647b7bce Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 21:00:07 +0900 Subject: [PATCH 12/16] =?UTF-8?q?chore:=20bottomSheet=20=ED=8C=A8=EB=94=A9?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/BottomSheet.css.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/BottomSheet/BottomSheet.css.ts b/src/components/BottomSheet/BottomSheet.css.ts index b4ea4856..34b0a558 100644 --- a/src/components/BottomSheet/BottomSheet.css.ts +++ b/src/components/BottomSheet/BottomSheet.css.ts @@ -9,7 +9,7 @@ export const titleWrapper = style({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - marginBottom: '30px', + padding: '20px 20px 30px 20px', }); globalStyle(`${titleWrapper} > div`, { ...fontVariant.title3, @@ -51,7 +51,6 @@ export const wrap = recipe({ width: '100%', background: color.white, zIndex: zIndices.modal, - padding: '20px', borderRadius: '14px 14px 0px 0px', flexDirection: 'column', justifyContent: 'flex-start', From 4a3c38e8ee2a3ce18e5a006021ec092c0b376fd6 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Sun, 9 Jul 2023 21:12:54 +0900 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20=EC=98=88=EC=A0=9C=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentSection/ContentSection.tsx | 63 +------------------ 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/src/app/board/components/ContentSection/ContentSection.tsx b/src/app/board/components/ContentSection/ContentSection.tsx index 1137bdb2..6ed542b6 100644 --- a/src/app/board/components/ContentSection/ContentSection.tsx +++ b/src/app/board/components/ContentSection/ContentSection.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { IconButton, Typography, Dropdown } from '@/components'; +import { IconButton, Typography } from '@/components'; import { getBoardStatusText } from '@/app/board/utils'; import { BoardDetail } from '@/api/types'; @@ -92,66 +92,7 @@ const ContentSection = ({ board }: Props) => { )} -
    - {content} - - - - - 서울 - - - 경기도 - - - 강원도 - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - - 8 - - - 9 - - - 10 - - - 11 - - - 12 - - - 13 - - - 14 - - - - {/* 바텀시트 내부에 위치한 드랍다운 테스트 해보기 */} -
    +
    {content}
    From ea170cbc6a9dd408d1a9dad5662b0352175f9ca6 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Mon, 10 Jul 2023 21:36:23 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat:=20useClickOutside=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useClickOutside/useClickOutside.test.tsx | 55 +++++++++++-------- src/hooks/useClickOutside/useClickOutside.ts | 53 +++++++++--------- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/hooks/useClickOutside/useClickOutside.test.tsx b/src/hooks/useClickOutside/useClickOutside.test.tsx index 187ca2cb..a792f685 100644 --- a/src/hooks/useClickOutside/useClickOutside.test.tsx +++ b/src/hooks/useClickOutside/useClickOutside.test.tsx @@ -4,32 +4,41 @@ import useClickOutside from './useClickOutside'; import userEvent from '@testing-library/user-event'; const TestComponent = ({ handleClickOutside }: { handleClickOutside: () => void }) => { - const ref = useClickOutside({ - onClickOutside: handleClickOutside, - }); + const ref = useClickOutside({ + onClickOutside: handleClickOutside, + }); - return ( -
    -
    target
    -
    nontarget
    -
    - ); + return ( +
    +
    overlay
    +
    target
    +
    nontarget
    +
    + ); }; describe('useClickOutside 테스트', () => { - test('ref로 지정한 영역 바깥 부분을 누를때 이벤트가 감지된다', async () => { - const handleClickOutside = vi.fn(); - render(); - const nonTarget = screen.getByText('nontarget'); - await userEvent.click(nonTarget); - expect(handleClickOutside).toHaveBeenCalled(); - }); + test('ref로 지정한 영역 바깥 부분을 누를때 이벤트가 감지된다', async () => { + const handleClickOutside = vi.fn(); + render(); + const nonTarget = screen.getByText('nontarget'); + await userEvent.click(nonTarget); + expect(handleClickOutside).toHaveBeenCalled(); + }); - test('ref로 지정한 영역을 누르면 이벤트가 감지되지 않는다', async () => { - const handleClickOutside = vi.fn(); - render(); - const target = screen.getByText('target'); - await userEvent.click(target); - expect(handleClickOutside).not.toHaveBeenCalled(); - }); + test('ref로 지정한 영역을 누르면 이벤트가 감지되지 않는다', async () => { + const handleClickOutside = vi.fn(); + render(); + const target = screen.getByText('target'); + await userEvent.click(target); + expect(handleClickOutside).not.toHaveBeenCalled(); + }); + + test('ref로 지정한 대상이 overlay-container에 없는 상황에서 클릭 대상이 overlay-container에 있으면 이벤트는 감지되지 않는다', async () => { + const handleClickOutside = vi.fn(); + render(); + const target = screen.getByText('overlay'); + await userEvent.click(target); + expect(handleClickOutside).not.toHaveBeenCalled(); + }); }); diff --git a/src/hooks/useClickOutside/useClickOutside.ts b/src/hooks/useClickOutside/useClickOutside.ts index 32ef218b..7b09cb41 100644 --- a/src/hooks/useClickOutside/useClickOutside.ts +++ b/src/hooks/useClickOutside/useClickOutside.ts @@ -1,18 +1,17 @@ import { useEffect, useRef } from 'react'; type ClickOutsideEvents = Pick< - WindowEventMap, - 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup' + WindowEventMap, + 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup' >; -/** - * @property {() => void} onClickOutside - 외부 클릭 이벤트 발생 시 실행할 함수 - * @property {keyof ClickOutsideEvents} event - 외부 클릭 이벤트 종류 - */ +const OVERLAY_CONTAINER = '#overlay-container'; interface Props { - onClickOutside: () => void; - event?: keyof ClickOutsideEvents; + /** 외부 클릭 이벤트 발생 시 실행할 함수 */ + onClickOutside: () => void; + /** 외부 클릭 이벤트 종류 */ + event?: keyof ClickOutsideEvents; } /** @@ -21,26 +20,28 @@ interface Props { * @param {keyof ClickOutsideEvents} props.event - 외부 클릭 이벤트 종류 * @returns {React.RefObject} 외부 클릭 이벤트를 감지할 element ref */ - const useClickOutside = ({ - onClickOutside, - event = 'mousedown', + onClickOutside, + event = 'mousedown', }: Props): React.RefObject => { - const ref = useRef(null); - - useEffect(() => { - const handleClickOutside = (e: ClickOutsideEvents[typeof event]) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - onClickOutside(); - } - }; - window.addEventListener(event, handleClickOutside); - return () => { - window.removeEventListener(event, handleClickOutside); - }; - }, [onClickOutside, event]); - - return ref; + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: ClickOutsideEvents[typeof event]) => { + if (!ref.current?.closest(OVERLAY_CONTAINER) && (e.target as HTMLElement).closest(OVERLAY_CONTAINER)) { + return; + } + if (ref.current && !ref.current.contains(e.target as Node)) { + onClickOutside(); + } + }; + window.addEventListener(event, handleClickOutside); + return () => { + window.removeEventListener(event, handleClickOutside); + }; + }, [onClickOutside, event]); + + return ref; }; export default useClickOutside; From 0657a1d979dffc12f8a0d3c1078a9d7230d5a74d Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Mon, 10 Jul 2023 22:01:19 +0900 Subject: [PATCH 15/16] =?UTF-8?q?chore:=20useDropdownBottomSheet=20?= =?UTF-8?q?=ED=9B=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useDropdownBottomSheet.test.ts | 21 +++++++++++++++++++ .../useDropdownBottomSheet.tsx | 6 ++++++ 2 files changed, 27 insertions(+) create mode 100644 src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.test.ts diff --git a/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.test.ts b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.test.ts new file mode 100644 index 00000000..5e9c107e --- /dev/null +++ b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.test.ts @@ -0,0 +1,21 @@ +import { renderHook } from '@/tests/test-utils'; +import useDropdownBottomSheet from './useDropdownBottomSheet'; + +describe('useDropdownBottomSheet 테스트', () => { + test('isDropdownOpen 값이 true가 되면 bottomSheetState는 true가 되고 그 반대인 경우에는 false가 된다', () => { + let openState = false; + const { result: bottomSheetState, rerender } = renderHook(() => + useDropdownBottomSheet({ + isDropdownOpen: openState, + onClose: () => null, + }), + ); + openState = true; + rerender(); + expect(bottomSheetState.current[1]).toBe(true); + + openState = false; + rerender(); + expect(bottomSheetState.current[1]).toBe(false); + }); +}); diff --git a/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx index 3eddf1cd..1673d8d4 100644 --- a/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx +++ b/src/components/Dropdown/hooks/useDropdownBottomSheet/useDropdownBottomSheet.tsx @@ -7,6 +7,7 @@ interface Props { } const useDropdownBottomSheet = ({ isDropdownOpen, onClose }: Props) => { + const firstRender = useRef(true); const [isOpen, setOpen, setClose] = useBooleanState(false); const closeStatus = useRef(false); @@ -36,6 +37,11 @@ const useDropdownBottomSheet = ({ isDropdownOpen, onClose }: Props) => { ); useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + return; + } + if (isDropdownOpen) { openBottomSheet(); } else { From f6ce23224d9158c6bc59f2bfbfc7c1d774f0f571 Mon Sep 17 00:00:00 2001 From: Yangseungchan Date: Mon, 10 Jul 2023 22:56:32 +0900 Subject: [PATCH 16/16] =?UTF-8?q?feat:=20BottomSheet=20=ED=8A=B9=EC=88=98?= =?UTF-8?q?=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet/BottomSheetView.tsx | 2 +- .../hooks/useModalControl/useModalControl.ts | 8 ++------ src/components/Dropdown/DropdownModal.tsx | 17 ++++++++++------- src/hooks/useClickOutside/useClickOutside.ts | 2 ++ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/components/BottomSheet/BottomSheetView.tsx b/src/components/BottomSheet/BottomSheetView.tsx index c9c888de..8578dcdc 100644 --- a/src/components/BottomSheet/BottomSheetView.tsx +++ b/src/components/BottomSheet/BottomSheetView.tsx @@ -21,7 +21,7 @@ const BottomSheetView = forwardRef( return ( <> -
    +
    {title}
    diff --git a/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts b/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts index bf1c2593..d4ebdd8c 100644 --- a/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts +++ b/src/components/BottomSheet/hooks/useModalControl/useModalControl.ts @@ -1,10 +1,10 @@ import { useState, useEffect, useRef } from 'react'; -import { useClickOutside } from '@/hooks'; interface Props { onClose: () => void; } const useModalControl = ({ onClose }: Props) => { + const ref = useRef(null); const [isOpen, setIsOpen] = useState(false); const closeStatus = useRef(false); const closeModalWithTransition = () => { @@ -14,11 +14,7 @@ const useModalControl = ({ onClose }: Props) => { const openModalWithTransition = () => { setIsOpen(true); }; - const ref = useClickOutside({ - onClickOutside: () => { - closeModalWithTransition(); - }, - }); + const handleTransitionEnd = (event: TransitionEvent) => { if (event.propertyName === 'transform' && closeStatus.current) { onClose(); diff --git a/src/components/Dropdown/DropdownModal.tsx b/src/components/Dropdown/DropdownModal.tsx index 4b8b6671..b9fff4e0 100644 --- a/src/components/Dropdown/DropdownModal.tsx +++ b/src/components/Dropdown/DropdownModal.tsx @@ -2,6 +2,7 @@ import cx from 'classnames'; import { HTMLAttributes, useMemo, forwardRef, useImperativeHandle } from 'react'; +import { createPortal } from 'react-dom'; import { Modal } from '@/components'; import { useDropdownContext } from './contexts/DropdownContext'; import { type DropdownMenuContextValue, DropdownMenuContextProvider } from './contexts/DropdownMenuContext'; @@ -29,13 +30,15 @@ const DropdownModal = forwardRef( return ( - {isOpen && ( - - - {children} - - - )} + {isOpen && + createPortal( + + + {children} + + , + document.getElementById('overlay-container') as HTMLElement, + )} ); }, diff --git a/src/hooks/useClickOutside/useClickOutside.ts b/src/hooks/useClickOutside/useClickOutside.ts index 7b09cb41..5604fc7b 100644 --- a/src/hooks/useClickOutside/useClickOutside.ts +++ b/src/hooks/useClickOutside/useClickOutside.ts @@ -28,9 +28,11 @@ const useClickOutside = ({ useEffect(() => { const handleClickOutside = (e: ClickOutsideEvents[typeof event]) => { + // ref가 overlay-container내에 없는데, overlay-container 안에서 발생한 이벤트 인 경우 if (!ref.current?.closest(OVERLAY_CONTAINER) && (e.target as HTMLElement).closest(OVERLAY_CONTAINER)) { return; } + if (ref.current && !ref.current.contains(e.target as Node)) { onClickOutside(); }