From ddda0b8624db676772aaf088dc7d3f9b40af0dcc Mon Sep 17 00:00:00 2001 From: belltalion Date: Wed, 10 Sep 2025 15:15:56 +0900 Subject: [PATCH 1/8] Feat radio components --- package.json | 1 + .../src/components/Radio/Radio.stories.tsx | 29 ++ .../Radio/__test__/index.browser.test.tsx | 89 ++++ .../components/src/components/Radio/index.tsx | 228 ++++++++++ .../RadioGroup/RadioGroup.stories.tsx | 43 ++ .../RadioGroup.browser.test.tsx.snap | 411 ++++++++++++++++++ .../__snapshots__/index.browser.test.tsx.snap | 411 ++++++++++++++++++ .../__tests__/index.browser.test.tsx | 163 +++++++ .../src/components/RadioGroup/index.tsx | 106 +++++ 9 files changed, 1481 insertions(+) create mode 100644 packages/components/src/components/Radio/Radio.stories.tsx create mode 100644 packages/components/src/components/Radio/__test__/index.browser.test.tsx create mode 100644 packages/components/src/components/Radio/index.tsx create mode 100644 packages/components/src/components/RadioGroup/RadioGroup.stories.tsx create mode 100644 packages/components/src/components/RadioGroup/__tests__/__snapshots__/RadioGroup.browser.test.tsx.snap create mode 100644 packages/components/src/components/RadioGroup/__tests__/__snapshots__/index.browser.test.tsx.snap create mode 100644 packages/components/src/components/RadioGroup/__tests__/index.browser.test.tsx create mode 100644 packages/components/src/components/RadioGroup/index.tsx diff --git a/package.json b/package.json index 3d0961c3..b3ca451d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/node": "^24.3.0", "happy-dom": "^18.0.1", "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "14.6.1", "@testing-library/jest-dom": "^6.7.0", "@devup-ui/vite-plugin": "workspace:*" }, diff --git a/packages/components/src/components/Radio/Radio.stories.tsx b/packages/components/src/components/Radio/Radio.stories.tsx new file mode 100644 index 00000000..794101aa --- /dev/null +++ b/packages/components/src/components/Radio/Radio.stories.tsx @@ -0,0 +1,29 @@ +import { Radio } from '.' + +export default { + title: 'Devfive/Radio', + component: Radio, +} + +export const Default = { + args: { + checked: undefined, + colors: { + primary: 'var(--primary)', + border: 'var(--border)', + text: 'var(--text)', + bg: 'var(--bg)', + hoverBg: 'var(--hoverBg)', + hoverBorder: 'var(--hoverBorder)', + hoverColor: 'var(--hoverColor)', + checkedBg: 'var(--checkedBg)', + checkedBorder: 'var(--checkedBorder)', + checkedColor: 'var(--checkedColor)', + disabledBg: 'var(--disabledBg)', + disabledColor: 'var(--disabledColor)', + }, + name: 'radio', + children: '옵션1', + variant: 'default', + }, +} diff --git a/packages/components/src/components/Radio/__test__/index.browser.test.tsx b/packages/components/src/components/Radio/__test__/index.browser.test.tsx new file mode 100644 index 00000000..74ad40ea --- /dev/null +++ b/packages/components/src/components/Radio/__test__/index.browser.test.tsx @@ -0,0 +1,89 @@ +import { render } from '@testing-library/react' + +import { Radio } from '../index' + +vi.mock('react', async (originImport: any) => { + const origin = await originImport() + return { + ...origin, + cache: vi.fn((arg) => arg), + } +}) +describe('Radio', () => { + it('should Radio snapshot', () => { + expect(render().container).toMatchSnapshot() + expect(render().container).toMatchSnapshot() + expect(render().container).toMatchSnapshot() + expect( + render().container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + + expect( + render().container, + ).toMatchSnapshot() + expect( + render().container, + ).toMatchSnapshot() + }) +}) diff --git a/packages/components/src/components/Radio/index.tsx b/packages/components/src/components/Radio/index.tsx new file mode 100644 index 00000000..677c860d --- /dev/null +++ b/packages/components/src/components/Radio/index.tsx @@ -0,0 +1,228 @@ +import { Box, Input, Text } from '@devup-ui/react' + +type RadioProps = Omit, 'type'> & { + checked?: boolean + classNames?: { + label?: string + } + color?: string + hoverColor?: string + styles?: { + label?: React.CSSProperties + } + colors?: { + primary?: string + border?: string + text?: string + bg?: string + hoverBg?: string + hoverBorder?: string + hoverColor?: string + checkedBg?: string + checkedBorder?: string + checkedColor?: string + disabledBg?: string + disabledColor?: string + } + variant?: 'default' | 'button' +} & ( + | { + variant?: 'default' + firstButton?: undefined + lastButton?: undefined + } + | { + variant: 'button' + firstButton?: boolean + lastButton?: boolean + } + ) + +export function Radio({ + className, + disabled, + children, + variant = 'default', + checked, + classNames, + styles, + style, + firstButton, + lastButton, + colors, + ...props +}: RadioProps) { + const isButton = variant === 'button' + return ( + + {isButton ? ( + + ) : ( + + )} + {variant === 'button' ? ( + + {children} + + ) : ( + + {children} + + )} + + ) +} diff --git a/packages/components/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/components/src/components/RadioGroup/RadioGroup.stories.tsx new file mode 100644 index 00000000..f6ad479e --- /dev/null +++ b/packages/components/src/components/RadioGroup/RadioGroup.stories.tsx @@ -0,0 +1,43 @@ +import { RadioGroup } from './index' + +export default { + title: 'Devfive/RadioGroup', + component: RadioGroup, +} + +export const Default = { + args: { + disabled: false, + name: 'radio', + colors: { + primary: 'var(--primary)', + border: 'var(--border)', + text: 'var(--text)', + bg: 'var(--bg)', + hoverBg: 'var(--hoverBg)', + hoverBorder: 'var(--hoverBorder)', + hoverColor: 'var(--hoverColor)', + checkedBg: 'var(--checkedBg)', + checkedBorder: 'var(--checkedBorder)', + checkedColor: 'var(--checkedColor)', + disabledBg: 'var(--disabledBg)', + disabledColor: 'var(--disabledColor)', + }, + options: [ + { + value: '1', + label: '옵션 1', + }, + { + value: '2', + label: '옵션 2', + }, + { + value: '3', + label: '옵션 3', + }, + ], + variant: 'default', + direction: 'row', + }, +} diff --git a/packages/components/src/components/RadioGroup/__tests__/__snapshots__/RadioGroup.browser.test.tsx.snap b/packages/components/src/components/RadioGroup/__tests__/__snapshots__/RadioGroup.browser.test.tsx.snap new file mode 100644 index 00000000..3d463bbe --- /dev/null +++ b/packages/components/src/components/RadioGroup/__tests__/__snapshots__/RadioGroup.browser.test.tsx.snap @@ -0,0 +1,411 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RadioGroup > should RadioGroup snapshot 1`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 2`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 3`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 4`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 5`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 6`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 7`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 8`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 9`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 10`] = ` +
+
+ + +
+
+`; diff --git a/packages/components/src/components/RadioGroup/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/RadioGroup/__tests__/__snapshots__/index.browser.test.tsx.snap new file mode 100644 index 00000000..3d463bbe --- /dev/null +++ b/packages/components/src/components/RadioGroup/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -0,0 +1,411 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RadioGroup > should RadioGroup snapshot 1`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 2`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 3`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 4`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 5`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 6`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 7`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 8`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 9`] = ` +
+
+ + +
+
+`; + +exports[`RadioGroup > should RadioGroup snapshot 10`] = ` +
+
+ + +
+
+`; diff --git a/packages/components/src/components/RadioGroup/__tests__/index.browser.test.tsx b/packages/components/src/components/RadioGroup/__tests__/index.browser.test.tsx new file mode 100644 index 00000000..7620cbbd --- /dev/null +++ b/packages/components/src/components/RadioGroup/__tests__/index.browser.test.tsx @@ -0,0 +1,163 @@ +import { act, render } from '@testing-library/react' +import { userEvent } from '@testing-library/user-event' + +import { RadioGroup } from '../index' + +vi.mock('react', async (originImport: any) => { + const origin = await originImport() + return { + ...origin, + cache: vi.fn((arg) => arg), + } +}) +describe('RadioGroup', () => { + const options = [ + { value: '1', label: '옵션 1' }, + { value: '2', label: '옵션 2' }, + ] + + it('should RadioGroup snapshot', () => { + expect(render().container).toMatchSnapshot() + expect( + render().container, + ).toMatchSnapshot() + expect( + render().container, + ).toMatchSnapshot() + expect( + render() + .container, + ).toMatchSnapshot() + expect( + render().container, + ).toMatchSnapshot() + expect( + render() + .container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + expect( + render( + , + ).container, + ).toMatchSnapshot() + }) + it('should change value when click', async () => { + const onChange = vi.fn() + const { getByText } = render( + , + ) + await act(async () => { + await userEvent.click(getByText('옵션 2')) + await userEvent.click(getByText('옵션 1')) + }) + expect(onChange).toBeCalledTimes(2) + expect(onChange).toHaveBeenNthCalledWith(1, '2') + expect(onChange).toHaveBeenNthCalledWith(2, '1') + }) + it('should have correct value with number values', async () => { + const numberOptions = [ + { value: 1, label: '옵션 1' }, + { value: 2, label: '옵션 2' }, + ] + const onChange = vi.fn() + const { getByText } = render( + , + ) + await act(async () => { + await userEvent.click(getByText('옵션 2')) + await userEvent.click(getByText('옵션 1')) + }) + expect(onChange).toBeCalledTimes(2) + expect(onChange).toHaveBeenNthCalledWith(1, '2') + expect(onChange).toHaveBeenNthCalledWith(2, '1') + }) + it('should have correct value with boolean values', async () => { + const booleanOptions = [ + { value: true, label: '옵션 1' }, + { value: false, label: '옵션 2' }, + ] + const onChange = vi.fn() + const { getByText } = render( + , + ) + await act(async () => { + await userEvent.click(getByText('옵션 2')) + await userEvent.click(getByText('옵션 1')) + }) + expect(onChange).toBeCalledTimes(2) + expect(onChange).toHaveBeenNthCalledWith(1, 'false') + expect(onChange).toHaveBeenNthCalledWith(2, 'true') + }) + it('should have correct value with value prop', async () => { + const onChange = vi.fn() + const { getByText } = render( + , + ) + await act(async () => { + await userEvent.click(getByText('옵션 2')) + }) + expect(onChange).toHaveBeenNthCalledWith(1, '2') + }) +}) diff --git a/packages/components/src/components/RadioGroup/index.tsx b/packages/components/src/components/RadioGroup/index.tsx new file mode 100644 index 00000000..2ee3e24f --- /dev/null +++ b/packages/components/src/components/RadioGroup/index.tsx @@ -0,0 +1,106 @@ +'use client' +import { Flex } from '@devup-ui/react' +import { useState } from 'react' + +import { Radio } from '../Radio' + +interface RadioGroupProps { + options: { + value: string | number | boolean + label: React.ReactNode + }[] + disabled?: boolean + direction?: 'row' | 'column' + variant?: 'default' | 'button' + style?: React.CSSProperties + value?: string | number | boolean + onChange?: (value: string | number | boolean) => void + defaultValue?: string | number | boolean + className?: string + colors?: { + primary?: string + border?: string + text?: string + bg?: string + hoverBg?: string + hoverBorder?: string + hoverColor?: string + checkedBg?: string + checkedBorder?: string + checkedColor?: string + disabledBg?: string + disabledColor?: string + } + classNames?: { + label?: string + container?: string + } + styles?: { + label?: React.CSSProperties + container?: React.CSSProperties + } +} +export function RadioGroup({ + disabled, + options, + direction = 'row', + variant = 'default', + style, + value, + onChange, + defaultValue, + colors, + className, + classNames, + styles, +}: RadioGroupProps) { + const [innerValue, setInnerValue] = useState( + value ? String(value) : defaultValue ? String(defaultValue) : undefined, + ) + const resultValue = value ? String(value) : (innerValue ?? '') + + function handleChange(_value: string) { + onChange?.(_value) + setInnerValue(_value) + } + + return ( + + {options.map(({ value: optionValue, label }, idx) => { + const stringValue = String(optionValue) + const props = { + checked: resultValue === stringValue, + disabled, + onChange: () => !disabled && handleChange(stringValue), + className, + classNames, + styles, + style, + } as const + return variant === 'button' ? ( + + {label} + + ) : ( + + {label} + + ) + })} + + ) +} From d8b0cad278ac155de5bfa5552be9485fe9895224 Mon Sep 17 00:00:00 2001 From: belltalion Date: Wed, 10 Sep 2025 15:17:53 +0900 Subject: [PATCH 2/8] Feat radio components --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2c63c14..70b6e2c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': specifier: ^24.3.1 version: 24.3.1 From cefe2d30d790f02902ee8bacae7292e4538d8741 Mon Sep 17 00:00:00 2001 From: belltalion Date: Wed, 10 Sep 2025 15:29:02 +0900 Subject: [PATCH 3/8] Fix snap --- .../__snapshots__/index.browser.test.tsx.snap | 218 ++++++++++ .../index.browser.test.tsx | 0 .../RadioGroup.browser.test.tsx.snap | 411 ------------------ 3 files changed, 218 insertions(+), 411 deletions(-) create mode 100644 packages/components/src/components/Radio/__tests__/__snapshots__/index.browser.test.tsx.snap rename packages/components/src/components/Radio/{__test__ => __tests__}/index.browser.test.tsx (100%) delete mode 100644 packages/components/src/components/RadioGroup/__tests__/__snapshots__/RadioGroup.browser.test.tsx.snap diff --git a/packages/components/src/components/Radio/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Radio/__tests__/__snapshots__/index.browser.test.tsx.snap new file mode 100644 index 00000000..9e29e522 --- /dev/null +++ b/packages/components/src/components/Radio/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -0,0 +1,218 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Radio > should Radio snapshot 1`] = ` +
+ +
+`; + +exports[`Radio > should Radio snapshot 2`] = ` +
+