From 48a0f04c2355cbac23a6954dd44dd48790365b36 Mon Sep 17 00:00:00 2001 From: ssi02014 Date: Tue, 3 Dec 2024 16:49:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(react):=20SwithCase=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=9C=EA=B8=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/hot-windows-divide.md | 5 ++ .../docs/react/components/DebounceWrapper.mdx | 2 +- .../DebounceWrapper/DebounceWrapper.spec.tsx | 4 +- .../src/components/DebounceWrapper/index.tsx | 53 ++++++++++++-- .../components/SwitchCase/SwitchCase.test.tsx | 71 ++++++++----------- .../react/src/components/SwitchCase/index.tsx | 46 ++++++++---- packages/react/src/hooks/useDebounce/index.ts | 4 +- 7 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 .changeset/hot-windows-divide.md diff --git a/.changeset/hot-windows-divide.md b/.changeset/hot-windows-divide.md new file mode 100644 index 00000000..b42ae4da --- /dev/null +++ b/.changeset/hot-windows-divide.md @@ -0,0 +1,5 @@ +--- +'@modern-kit/react': patch +--- + +fix(react): SwithCase 인터페이스 개선 - @ssi02014 diff --git a/docs/docs/react/components/DebounceWrapper.mdx b/docs/docs/react/components/DebounceWrapper.mdx index 07bfd793..91904e44 100644 --- a/docs/docs/react/components/DebounceWrapper.mdx +++ b/docs/docs/react/components/DebounceWrapper.mdx @@ -3,7 +3,7 @@ import { DebounceWrapper } from '@modern-kit/react'; # DebounceWrapper -자식 요소에서 발생하는 이벤트`(ex: Click Event)`를 debounce해주는 유틸 컴포넌트입니다. +자식 컴포넌트의 이벤트 핸들러에 디바운스(debounce)를 선언적으로 적용할 수 있는 컴포넌트입니다.
diff --git a/packages/react/src/components/DebounceWrapper/DebounceWrapper.spec.tsx b/packages/react/src/components/DebounceWrapper/DebounceWrapper.spec.tsx index c072b66d..6675f888 100644 --- a/packages/react/src/components/DebounceWrapper/DebounceWrapper.spec.tsx +++ b/packages/react/src/components/DebounceWrapper/DebounceWrapper.spec.tsx @@ -62,7 +62,7 @@ const TestComponentWithInput = ({ capture, wait }: TestComponentProps) => { }; describe('DebounceWrapper Component', () => { - it('should debounce click event from child element', async () => { + it('자식 요소의 onClick 이벤트를 디바운스해야 합니다.', async () => { const mockFn = vi.fn(); // https://github.com/testing-library/user-event/issues/833 const { user } = renderSetup( @@ -90,7 +90,7 @@ describe('DebounceWrapper Component', () => { expect(mockFn).toBeCalledTimes(2); }); - it('should debounce change event from child element', async () => { + it('자식 요소의 onChange 이벤트를 디바운스해야 합니다.', async () => { const { user } = renderSetup( , { delay: null } diff --git a/packages/react/src/components/DebounceWrapper/index.tsx b/packages/react/src/components/DebounceWrapper/index.tsx index 99b5ceee..89d443c2 100644 --- a/packages/react/src/components/DebounceWrapper/index.tsx +++ b/packages/react/src/components/DebounceWrapper/index.tsx @@ -1,5 +1,6 @@ import { Children, cloneElement } from 'react'; import { DebounceParameters, useDebounce } from '../../hooks/useDebounce'; +import { isFunction } from '@modern-kit/utils'; export interface DebounceWrapperProps { children: JSX.Element; @@ -8,20 +9,65 @@ export interface DebounceWrapperProps { options?: DebounceParameters[2]; } +/** + * @description 자식 컴포넌트의 이벤트 핸들러에 디바운스를 선언적으로 적용할 수 있는 컴포넌트입니다. + * + * @param {DebounceWrapperProps} props - `DebounceWrapper` 컴포넌트의 속성 + * @param {JSX.Element} props.children - 디바운스를 적용할 자식 컴포넌트 + * @param {string} props.capture - 디바운스를 적용할 이벤트 핸들러 이름 (예: 'onClick', 'onChange') + * @param {number} props.wait - 디바운스가 적용될 시간(ms)입니다. 이 시간이 지나면 콜백이 실행됩니다. + * @param {object} props.options - 디바운스 동작에 영향을 주는 추가 옵션입니다. + * + * @returns {JSX.Element} - 디바운스가 적용된 자식 컴포넌트 + * + * @example + * ```tsx + * // onClick debounce + * + * + * + * ``` + * + * @example + * ```tsx + * // onChange debounce + * const [debouncedValue, setDebouncedValue] = useState(''); + * + * const onChange = (value: string) => { + * setDebouncedValue(value); + * }; + * + * // 컴포넌트로 래핑이 필요합니다 + * const Input = ({ onChange }: { onChange: (value: string) => void }) => { + * const [value, setValue] = useState(''); + * + * const handleChange = (e: ChangeEvent) => { + * setValue(e.target.value); + * onChange(e.target.value); + * }; + * + * return ; + * }; + * + * + * + * + * ``` + */ export const DebounceWrapper = ({ children, capture, wait, options, -}: DebounceWrapperProps) => { - // If children is a valid element, returns that element. Otherwise, throws an error. +}: DebounceWrapperProps): JSX.Element => { const child = Children.only(children); + const debouncedCallback = useDebounce( (...args: any[]) => { const childProps = child?.props; if (!childProps) return; - if (typeof childProps[capture] === 'function') { + if (isFunction(childProps[capture])) { return childProps[capture](...args); } }, @@ -29,7 +75,6 @@ export const DebounceWrapper = ({ options ); - // cloneElement lets you create a new React element using another element as a starting point. return cloneElement(child, { [capture]: debouncedCallback, }); diff --git a/packages/react/src/components/SwitchCase/SwitchCase.test.tsx b/packages/react/src/components/SwitchCase/SwitchCase.test.tsx index 5cf5b8d9..eeb51f21 100644 --- a/packages/react/src/components/SwitchCase/SwitchCase.test.tsx +++ b/packages/react/src/components/SwitchCase/SwitchCase.test.tsx @@ -3,50 +3,41 @@ import { screen } from '@testing-library/react'; import { renderSetup } from '../../_internal/test/renderSetup'; import { SwitchCase } from '.'; +const TestComponent = ({ value }: { value: string }) => { + return ( + A, b:
B
}} + defaultComponent={
Default
} + /> + ); +}; + describe('SwitchCase', () => { - it('should SwitchCase component receive condition.', () => { - renderSetup( - case no.1 }} /> - ); - const CaseOneComponent = screen.queryByRole('button', { - name: 'case no.1', - }); - - expect(CaseOneComponent).toBeInTheDocument(); + it(`value가 'a'일 때 'A'가 렌더링되어야 합니다.`, () => { + renderSetup(); + + expect(screen.getByText('A')).toBeInTheDocument(); + + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('Default')).not.toBeInTheDocument(); }); - it('should render defaultCaseComponent when there is no matched condition.', () => { - renderSetup( - case no.1 }} - defaultCaseComponent={} - /> - ); - - const CaseOneComponent = screen.queryByRole('button', { - name: 'case no.1', - }); - const DefaultCaseComponent = screen.queryByRole('button', { - name: 'default component', - }); - - expect(CaseOneComponent).not.toBeInTheDocument(); - expect(DefaultCaseComponent).toBeInTheDocument(); + it(`value가 'b'일 때 'B'가 렌더링되어야 합니다.`, () => { + renderSetup(); + + expect(screen.getByText('B')).toBeInTheDocument(); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('Default')).not.toBeInTheDocument(); }); - it('should render defaultCaseComponent when condition is nullable.', () => { - renderSetup( - default component} - /> - ); - - const DefaultCaseComponent = screen.queryByRole('button', { - name: 'default component', - }); - expect(DefaultCaseComponent).toBeInTheDocument(); + it(`일치하는 value가 없을 때 'Default'가 렌더링되어야 합니다.`, () => { + renderSetup(); + + expect(screen.getByText('Default')).toBeInTheDocument(); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); }); }); diff --git a/packages/react/src/components/SwitchCase/index.tsx b/packages/react/src/components/SwitchCase/index.tsx index 499cb6e7..27e98798 100644 --- a/packages/react/src/components/SwitchCase/index.tsx +++ b/packages/react/src/components/SwitchCase/index.tsx @@ -1,21 +1,39 @@ +import { isNil } from '@modern-kit/utils'; import React from 'react'; -interface SwitchCaseProps { - condition: Condition | null; - cases: Partial>; - defaultCaseComponent?: JSX.Element | null; +interface SwitchCaseProps { + value: Case | null | undefined; + caseBy: Record; + defaultComponent?: React.ReactNode; } -export const SwitchCase = ({ - condition, - cases, - defaultCaseComponent = null, -}: SwitchCaseProps) => { - if (condition == null) { - return {defaultCaseComponent}; +/** + * @description value 값에 따라 다른 컴포넌트를 `Switch` 형태로 조건부 렌더링하는 컴포넌트입니다. + * + * @param {SwitchCaseProps} props - `SwitchCase` 컴포넌트의 속성 + * @param {Case | null | undefined} props.value - 렌더링할 케이스를 결정하는 값 + * @param {Record} props.caseBy - `value` 값에 대응하는 컴포넌트들을 담은 객체 + * @param {React.ReactNode} props.defaultComponent - `value`가 `null`이거나 `caseBy`에 해당하는 컴포넌트가 없을 때 렌더링할 기본 컴포넌트 + * + * @returns {React.ReactNode} - 조건부로 렌더링된 컴포넌트 + * + * @example + * ```tsx + * , error: }} + * defaultComponent={} + * /> + * ``` + */ +export const SwitchCase = ({ + caseBy, + value, + defaultComponent = null, +}: SwitchCaseProps): React.ReactNode => { + if (isNil(value)) { + return defaultComponent; } - return ( - {cases[condition] ?? defaultCaseComponent} - ); + return caseBy[value] ?? defaultComponent; }; diff --git a/packages/react/src/hooks/useDebounce/index.ts b/packages/react/src/hooks/useDebounce/index.ts index 14d7473a..b4af63cf 100644 --- a/packages/react/src/hooks/useDebounce/index.ts +++ b/packages/react/src/hooks/useDebounce/index.ts @@ -15,7 +15,7 @@ export type DebounceReturnType = ReturnType< * * @param {DebounceParameters[0]} callback - 디바운스 처리할 콜백 함수입니다. * @param {DebounceParameters[1]} wait - 디바운스가 적용될 시간(ms)입니다. 이 시간이 지나면 콜백이 실행됩니다. - * @param {DebounceParameters[2]} [options={}] - 디바운스 동작에 영향을 주는 추가 옵션입니다. `leading(default: false)`, `trailing(default: true)`, `maxWait` 옵션을 받을 수 있습니다. + * @param {DebounceParameters[2]} [options] - 디바운스 동작에 영향을 주는 추가 옵션입니다. `leading(default: false)`, `trailing(default: true)`, `maxWait` 옵션을 받을 수 있습니다. * * @returns {DebounceReturnType} 디바운스 처리된 콜백 함수를 반환합니다. * @@ -27,7 +27,7 @@ export type DebounceReturnType = ReturnType< export function useDebounce( callback: T, wait: DebounceParameters[1], - options: DebounceParameters[2] = {} + options?: DebounceParameters[2] ): DebounceReturnType { const callbackAction = usePreservedCallback(callback); const preservedOptions = usePreservedState(options);