From 87d75aa82da7f6d27c571025a3e7b52a3f156b69 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Wed, 21 Aug 2024 22:23:01 +0900 Subject: [PATCH] =?UTF-8?q?refactor(ui/system):=20createContext=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EC=9D=98=20=EC=9D=B8=EC=9E=90=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=84=EB=9E=B5=ED=99=94,=20=EB=B0=98=ED=99=98=EA=B0=92=20Pr?= =?UTF-8?q?ovider,=20useContext=EC=9D=B4=20=ED=8F=AC=ED=95=A8=EB=90=9C=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(ui/system): createContext 함수의 인자를 간략화, 반환값 Provider, useContext이 포함된 배열로 변경 * refactor(ui/components): createContext 변경사항 반영 --- .../checkbox/checkbox.primitive.tsx | 33 ++++------ .../ui/src/components/menu/menu-content.tsx | 6 +- .../ui/src/components/menu/menu-trigger.tsx | 6 +- .../ui/src/components/menu/menu.context.ts | 7 +-- packages/ui/src/components/menu/menu.tsx | 7 ++- .../ui/src/components/modal/modal-close.tsx | 6 +- .../ui/src/components/modal/modal-overlay.tsx | 6 +- .../ui/src/components/modal/modal-portal.tsx | 4 +- .../ui/src/components/modal/modal-topbar.tsx | 6 +- .../ui/src/components/modal/modal-trigger.tsx | 6 +- .../ui/src/components/modal/modal.context.ts | 6 +- packages/ui/src/components/modal/modal.tsx | 8 +-- packages/ui/src/system/create-context.tsx | 60 ++++++++----------- 13 files changed, 83 insertions(+), 78 deletions(-) diff --git a/packages/ui/src/components/checkbox/checkbox.primitive.tsx b/packages/ui/src/components/checkbox/checkbox.primitive.tsx index ebb57fc..ac1fc84 100644 --- a/packages/ui/src/components/checkbox/checkbox.primitive.tsx +++ b/packages/ui/src/components/checkbox/checkbox.primitive.tsx @@ -15,17 +15,15 @@ import { } from '../../system'; import { mergeFns, mergeStyles } from '../../utils'; +const CHECKBOX_NAME = 'Checkbox'; + type CheckboxContextValue = { state: boolean; disabled?: boolean; }; const [CheckboxProvider, useCheckboxContext] = - createContext({ - name: 'CheckboxContext', - hookName: 'useCheckboxContext', - providerName: '', - }); + createContext(CHECKBOX_NAME); type CheckboxProps = HTMLFavolinkPropsWithout< 'button', @@ -68,9 +66,7 @@ const Checkbox = forwardRef( useEffect(() => { const form = button?.form; - if (!form) { - return; - } + if (!form) return; function reset() { setChecked(initialCheckedStateRef.current); @@ -84,7 +80,7 @@ const Checkbox = forwardRef( }, [button, setChecked]); return ( - + ( {...restProps} ref={composedRefs} onKeyDown={mergeFns(props.onKeyDown, (event) => { - if (event.key === 'Enter') { - event.preventDefault(); - } + if (event.key === 'Enter') event.preventDefault(); })} onClick={mergeFns(props.onClick, (event) => { setChecked(!checked); @@ -108,9 +102,8 @@ const Checkbox = forwardRef( hasConsumerStoppedPropagation.current = event.isPropagationStopped(); - if (!hasConsumerStoppedPropagation.current) { + if (!hasConsumerStoppedPropagation.current) event.stopPropagation(); - } } })} /> @@ -131,13 +124,15 @@ const Checkbox = forwardRef( }, ); -Checkbox.displayName = 'Checkbox'; +Checkbox.displayName = CHECKBOX_NAME; + +const INDICATOR_NAME = 'CheckboxIndicator'; type CheckboxIndicatorProps = HTMLFavolinkProps<'span'>; const CheckboxIndicator = forwardRef( function CheckboxIndicator(props, forwardedRef) { - const context = useCheckboxContext(); + const context = useCheckboxContext(INDICATOR_NAME); return ( context.state && ( @@ -153,7 +148,7 @@ const CheckboxIndicator = forwardRef( }, ); -CheckboxIndicator.displayName = 'CheckboxIndicator'; +CheckboxIndicator.displayName = INDICATOR_NAME; type BubbleInputProps = HTMLProps<'input'> & { bubbles: boolean; @@ -176,9 +171,7 @@ function BubbleInput(props: BubbleInputProps) { ) as PropertyDescriptor; const setChecked = checkedDescriptor.set?.bind(input); - if (prevChecked === checked || !setChecked) { - return; - } + if (prevChecked === checked || !setChecked) return; const event = new Event('click', { bubbles }); diff --git a/packages/ui/src/components/menu/menu-content.tsx b/packages/ui/src/components/menu/menu-content.tsx index c0a0b3c..f094365 100644 --- a/packages/ui/src/components/menu/menu-content.tsx +++ b/packages/ui/src/components/menu/menu-content.tsx @@ -4,6 +4,8 @@ import { type Placement, usePosition } from '../../hooks'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { composeRefs, cx } from '../../utils'; +const CONTENT_NAME = 'MenuContent'; + export type MenuContentProps = HTMLFavolinkProps<'div'> & { placement?: Placement; sideOffset?: number; @@ -20,7 +22,7 @@ export const MenuContent = forwardRef( ...restProps } = props; - const context = useMenuContext(); + const context = useMenuContext(CONTENT_NAME); const coordinate = usePosition( context.open, @@ -48,3 +50,5 @@ export const MenuContent = forwardRef( ); }, ); + +MenuContent.displayName = CONTENT_NAME; diff --git a/packages/ui/src/components/menu/menu-trigger.tsx b/packages/ui/src/components/menu/menu-trigger.tsx index 2e392dc..5b9c6d9 100644 --- a/packages/ui/src/components/menu/menu-trigger.tsx +++ b/packages/ui/src/components/menu/menu-trigger.tsx @@ -2,13 +2,15 @@ import { useMenuContext } from './menu.context'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { composeRefs, cx, mergeFns } from '../../utils'; +const TRIGGER_NAME = 'MenuTrigger'; + export type MenuTriggerProps = HTMLFavolinkProps<'button'>; export const MenuTrigger = forwardRef( function MenuTrigger(props, ref) { const { children, className, onClick, ...restProps } = props; - const context = useMenuContext(); + const context = useMenuContext(TRIGGER_NAME); return ( ( ); }, ); + +MenuTrigger.displayName = TRIGGER_NAME; diff --git a/packages/ui/src/components/menu/menu.context.ts b/packages/ui/src/components/menu/menu.context.ts index 9412d33..863560b 100644 --- a/packages/ui/src/components/menu/menu.context.ts +++ b/packages/ui/src/components/menu/menu.context.ts @@ -8,8 +8,5 @@ type MenuContextValue = { containerRef: Ref; }; -export const [MenuProvider, useMenuContext] = createContext({ - name: 'MenuContentContext', - hookName: 'useMenuContentContext', - providerName: '', -}); +export const [MenuProvider, useMenuContext] = + createContext('Menu'); diff --git a/packages/ui/src/components/menu/menu.tsx b/packages/ui/src/components/menu/menu.tsx index b380cbb..f28d6d7 100644 --- a/packages/ui/src/components/menu/menu.tsx +++ b/packages/ui/src/components/menu/menu.tsx @@ -22,7 +22,12 @@ export function Menu(props: MenuProps) { const onOpenChange = onExternalOnOpenChange ?? onInternalOnOpenChange; return ( - + {children} ); diff --git a/packages/ui/src/components/modal/modal-close.tsx b/packages/ui/src/components/modal/modal-close.tsx index 7d77643..9a44f30 100644 --- a/packages/ui/src/components/modal/modal-close.tsx +++ b/packages/ui/src/components/modal/modal-close.tsx @@ -2,13 +2,15 @@ import { useModalContext } from './modal.context'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { cx, mergeFns } from '../../utils'; +const CLOSE_NAME = 'ModalClose'; + export type ModalCloseProps = HTMLFavolinkProps<'button'>; export const ModalClose = forwardRef( function ModalClose(props, ref) { const { children, className, onClick, ...restProps } = props; - const { onOpenChange } = useModalContext(); + const { onOpenChange } = useModalContext(CLOSE_NAME); return ( ( ); }, ); + +ModalClose.displayName = CLOSE_NAME; diff --git a/packages/ui/src/components/modal/modal-overlay.tsx b/packages/ui/src/components/modal/modal-overlay.tsx index 0b8592f..556f1c4 100644 --- a/packages/ui/src/components/modal/modal-overlay.tsx +++ b/packages/ui/src/components/modal/modal-overlay.tsx @@ -3,13 +3,15 @@ import { useModalContext } from './modal.context'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { cx, mergeFns } from '../../utils'; +const OVERLAY_NAME = 'ModalOverlay'; + export type ModalOverlayProps = HTMLFavolinkProps<'div'>; export const ModalOverlay = forwardRef( function ModalOverlay(props, ref) { const { className, onClick, ...restProps } = props; - const { onOpenChange, closeOnOverlayClick } = useModalContext(); + const { onOpenChange, closeOnOverlayClick } = useModalContext(OVERLAY_NAME); return ( ( ); }, ); + +ModalOverlay.displayName = OVERLAY_NAME; diff --git a/packages/ui/src/components/modal/modal-portal.tsx b/packages/ui/src/components/modal/modal-portal.tsx index 34be6ea..2946315 100644 --- a/packages/ui/src/components/modal/modal-portal.tsx +++ b/packages/ui/src/components/modal/modal-portal.tsx @@ -2,12 +2,14 @@ import { Children } from 'react'; import { useModalContext } from './modal.context'; import { Portal, type PortalProps } from '../portal/'; +const PORTAL_NAME = 'ModalPortal'; + export type ModalPortalProps = PortalProps; export function ModalPortal(props: ModalPortalProps) { const { children, ...restProps } = props; - const { onOpen } = useModalContext(); + const { onOpen } = useModalContext(PORTAL_NAME); return ( onOpen && diff --git a/packages/ui/src/components/modal/modal-topbar.tsx b/packages/ui/src/components/modal/modal-topbar.tsx index 83a6ae8..b585c2e 100644 --- a/packages/ui/src/components/modal/modal-topbar.tsx +++ b/packages/ui/src/components/modal/modal-topbar.tsx @@ -5,6 +5,8 @@ import { ChevronLeftIcon, CloseIcon } from '../../icons'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { cx, mergeFns } from '../../utils'; +const TOPBAR_NAME = 'ModalTopbar'; + export type ModalTopbarProps = HTMLFavolinkProps<'div'> & styles.ModalTopbarVariants & { onLeftIconClick?: (event: MouseEvent) => void; @@ -21,7 +23,7 @@ export const ModalTopbar = forwardRef( ...restProps } = props; - const { onOpenChange } = useModalContext(); + const { onOpenChange } = useModalContext(TOPBAR_NAME); const isCouple = layout !== 'single'; @@ -51,3 +53,5 @@ export const ModalTopbar = forwardRef( ); }, ); + +ModalTopbar.displayName = TOPBAR_NAME; diff --git a/packages/ui/src/components/modal/modal-trigger.tsx b/packages/ui/src/components/modal/modal-trigger.tsx index 13a5948..d97a092 100644 --- a/packages/ui/src/components/modal/modal-trigger.tsx +++ b/packages/ui/src/components/modal/modal-trigger.tsx @@ -2,13 +2,15 @@ import { useModalContext } from './modal.context'; import { type HTMLFavolinkProps, favolink, forwardRef } from '../../system'; import { cx, mergeFns } from '../../utils'; +const TRIGGER_NAME = 'ModalTrigger'; + export type ModalTriggerProps = HTMLFavolinkProps<'button'>; export const ModalTrigger = forwardRef( function MenuTrigger(props, ref) { const { className, children, onClick, ...restProps } = props; - const { onOpenChange } = useModalContext(); + const { onOpenChange } = useModalContext(TRIGGER_NAME); return ( ( ); }, ); + +ModalTrigger.displayName = TRIGGER_NAME; diff --git a/packages/ui/src/components/modal/modal.context.ts b/packages/ui/src/components/modal/modal.context.ts index 7a40071..c65f253 100644 --- a/packages/ui/src/components/modal/modal.context.ts +++ b/packages/ui/src/components/modal/modal.context.ts @@ -7,8 +7,4 @@ export type ModalContextValue = { }; export const [ModalProvider, useModalContext] = - createContext({ - name: 'ModalContext', - hookName: 'useModalContext', - providerName: '', - }); + createContext('Modal'); diff --git a/packages/ui/src/components/modal/modal.tsx b/packages/ui/src/components/modal/modal.tsx index e616607..0c5160b 100644 --- a/packages/ui/src/components/modal/modal.tsx +++ b/packages/ui/src/components/modal/modal.tsx @@ -20,11 +20,9 @@ export function Modal(props: ModalProps) { return ( {children} diff --git a/packages/ui/src/system/create-context.tsx b/packages/ui/src/system/create-context.tsx index 010def0..acf32ae 100644 --- a/packages/ui/src/system/create-context.tsx +++ b/packages/ui/src/system/create-context.tsx @@ -1,53 +1,43 @@ import { - type Context, - type Provider, + type ReactNode, createContext as createReactContext, + useMemo, useContext as useReactContext, } from 'react'; -type CreatContextOptions = { - name?: string; - hookName?: string; - providerName?: string; - errorMessage?: string; - defaultValue?: T; -}; - -type CreateContextReturn = [Provider, () => T, Context]; +export function createContext( + rootComponentName: string, + defaultValue?: T, +) { + const Context = createReactContext(defaultValue); -function getErrorMessage(hook: string, provider: string) { - return `${hook} returned \`undefined\`. Seems you forgot to wrap component within ${provider}`; -} + function Provider(props: T & { children: ReactNode }) { + const { children, ...context } = props; -export function createContext(options: CreatContextOptions) { - const { - name, - hookName = 'useContext', - providerName = 'Provider', - errorMessage, - defaultValue, - } = options; + // eslint-disable-next-line react-hooks/exhaustive-deps + const value = useMemo(() => context, Object.values(context)) as T; - const Context = createReactContext(defaultValue); + return {children}; + } - Context.displayName = name; + Provider.displayName = rootComponentName + 'Provider'; - function useContext() { + function useContext(consumerName: string) { const context = useReactContext(Context); - if (!context) { - const error = new Error( - errorMessage ?? getErrorMessage(hookName, providerName), - ); + if (context) return context; + + if (defaultValue !== undefined) return defaultValue; - error.name = 'ContextError'; - Error.captureStackTrace(error, useContext); + const error = new Error( + `\`${consumerName}\` must be used within \`${rootComponentName}\``, + ); - throw error; - } + error.name = 'ContextError'; + Error.captureStackTrace(error, useContext); - return context; + throw error; } - return [Context.Provider, useContext, Context] as CreateContextReturn; + return [Provider, useContext] as const; }