From f6db135749eae0ac19322f12026cb9cabfb71dc2 Mon Sep 17 00:00:00 2001 From: indianapoly Date: Tue, 16 Sep 2025 17:08:32 +0900 Subject: [PATCH 01/18] feat: checkbox --- .../src/components/Checkbox/CheckIcon.tsx | 24 ++++ .../components/Checkbox/Checkbox.stories.tsx | 27 ++++ .../src/components/Checkbox/index.tsx | 118 ++++++++++++++++++ .../CheckboxLayer/CheckboxLayer.stories.tsx | 118 ++++++++++++++++++ .../src/components/CheckboxLayer/index.tsx | 81 ++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 packages/components/src/components/Checkbox/CheckIcon.tsx create mode 100644 packages/components/src/components/Checkbox/Checkbox.stories.tsx create mode 100644 packages/components/src/components/Checkbox/index.tsx create mode 100644 packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx create mode 100644 packages/components/src/components/CheckboxLayer/index.tsx diff --git a/packages/components/src/components/Checkbox/CheckIcon.tsx b/packages/components/src/components/Checkbox/CheckIcon.tsx new file mode 100644 index 00000000..9951224a --- /dev/null +++ b/packages/components/src/components/Checkbox/CheckIcon.tsx @@ -0,0 +1,24 @@ +import { SVGProps } from 'react' + +type CheckIconProps = SVGProps & { + color: string +} + +export function CheckIcon({ color, ...props }: CheckIconProps) { + return ( + + + + ) +} diff --git a/packages/components/src/components/Checkbox/Checkbox.stories.tsx b/packages/components/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 00000000..38191305 --- /dev/null +++ b/packages/components/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from '@storybook/react-vite' + +import { Checkbox } from '.' + +type Story = StoryObj + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta: Meta = { + title: 'Devfive/Checkbox', + component: Checkbox, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export const Default: Story = { + args: { + children: 'Checkbox', + disabled: false, + }, +} + +export default meta diff --git a/packages/components/src/components/Checkbox/index.tsx b/packages/components/src/components/Checkbox/index.tsx new file mode 100644 index 00000000..e5935a9e --- /dev/null +++ b/packages/components/src/components/Checkbox/index.tsx @@ -0,0 +1,118 @@ +import { Box, css, Flex, Input, Text } from '@devup-ui/react' +import { ComponentProps } from 'react' + +import { CheckIcon } from './CheckIcon' + +interface CheckboxProps + extends Omit, 'type' | 'onChange'> { + children: React.ReactNode + onChange?: (checked: boolean) => void + variant?: 'primary' | 'default' + label: string +} + +export function Checkbox({ + children, + disabled, + checked, + onChange, + variant = 'primary', + label, + ...props +}: CheckboxProps) { + return ( + + + !disabled && onChange?.(e.target.checked)} + styleOrder={1} + type="checkbox" + {...props} + /> + {checked && ( + + )} + + + + + ) +} diff --git a/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx b/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx new file mode 100644 index 00000000..0420ce23 --- /dev/null +++ b/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx @@ -0,0 +1,118 @@ +import { Meta, StoryObj } from '@storybook/react-vite' + +import { CheckboxLayer } from '.' + +type Story = StoryObj + +const meta: Meta = { + title: 'Devfive/CheckboxLayer', + component: CheckboxLayer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + argTypes: { + onCheckboxChange: { action: 'checkbox changed' }, + }, +} + +export const RowLayout: Story = { + args: { + checkboxes: [ + { id: 'option1', value: '옵션 1 값', label: '옵션 1' }, + { + id: 'option2', + value: ( + + 파란색 텍스트 + + ), + label: '옵션 2', + }, + { + id: 'option3', + value: ( +
+ 🎉 + 이모지와 텍스트 +
+ ), + label: '옵션 3', + }, + { id: 'option4', value: 42, label: '옵션 4', disabled: true }, + { + id: 'option5', + value: ( + + ), + label: '옵션 5', + disabled: true, + checked: true, + }, + ], + flexDir: 'row', + defaultCheckedIds: ['option2', 'option5'], // 체크됨, disabled and checked + onCheckboxChange: (event) => { + console.info('체크박스 변경됨:', event) + console.info( + `ID: ${event.id}, Value: ${event.value}, Checked: ${event.checked}`, + ) + console.info('전체 선택된 값들:', event.checkedValues) + }, + }, +} + +export const ColumnLayout: Story = { + args: { + checkboxes: [ + { id: 'option1', value: '옵션 1 값', label: '옵션 1' }, + { + id: 'option2', + value: ( + + 파란색 텍스트 + + ), + label: '옵션 2', + }, + { + id: 'option3', + value: ( +
+ 🎉 + 이모지와 텍스트 +
+ ), + label: '옵션 3', + }, + { id: 'option4', value: 42, label: '옵션 4', disabled: true }, + { + id: 'option5', + value: ( + + ), + label: '옵션 5', + disabled: true, + checked: true, + }, + ], + flexDir: 'column', + defaultCheckedIds: ['option2', 'option5'], // 체크됨, disabled and checked + onCheckboxChange: (event) => { + console.info('체크박스 변경됨:', event) + console.info( + `ID: ${event.id}, Value: ${event.value}, Checked: ${event.checked}`, + ) + console.info('전체 선택된 값들:', event.checkedValues) + }, + }, +} + +export default meta diff --git a/packages/components/src/components/CheckboxLayer/index.tsx b/packages/components/src/components/CheckboxLayer/index.tsx new file mode 100644 index 00000000..86168669 --- /dev/null +++ b/packages/components/src/components/CheckboxLayer/index.tsx @@ -0,0 +1,81 @@ +import { Flex } from '@devup-ui/react' +import { useState } from 'react' + +import { Checkbox } from '../Checkbox' + +export interface CheckboxItem { + id: string + value: React.ReactNode + label: string + disabled?: boolean + checked?: boolean +} + +export interface CheckboxChangeEvent { + id: string + value: React.ReactNode + checked: boolean + checkedValues: React.ReactNode[] +} + +export interface CheckBoxLayerProps { + checkboxes: CheckboxItem[] + flexDir: 'row' | 'column' + gap?: number + onCheckboxChange?: (event: CheckboxChangeEvent) => void + defaultCheckedIds?: string[] + variant?: 'primary' | 'default' +} + +export function CheckboxLayer({ + checkboxes, + flexDir, + gap, + onCheckboxChange, + defaultCheckedIds = [], + variant = 'primary', +}: CheckBoxLayerProps) { + const [checkedIds, setCheckedIds] = useState(defaultCheckedIds) + + const handleCheckboxChange = ( + id: string, + value: React.ReactNode, + checked: boolean, + ) => { + const updatedIds = checked + ? [...checkedIds, id] + : checkedIds.filter((checkedId) => checkedId !== id) + + setCheckedIds(updatedIds) + + const checkedValues = updatedIds + .map((checkedId) => checkboxes.find((cb) => cb.id === checkedId)?.value) + .filter((val): val is React.ReactNode => val !== undefined) + + onCheckboxChange?.({ + id, + value, + checked, + checkedValues, + }) + } + + return ( + + {checkboxes.map((checkbox) => ( + + handleCheckboxChange(checkbox.id, checkbox.value, checked) + } + variant={variant} + > + {checkbox.value} + + ))} + + ) +} From e28ca97a471868dc677162ca02616cafb99c7fb1 Mon Sep 17 00:00:00 2001 From: indianapoly Date: Wed, 17 Sep 2025 10:04:48 +0900 Subject: [PATCH 02/18] update storybook scenario --- .../components/Checkbox/Checkbox.stories.tsx | 1 + .../CheckboxLayer/CheckboxLayer.stories.tsx | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/components/src/components/Checkbox/Checkbox.stories.tsx b/packages/components/src/components/Checkbox/Checkbox.stories.tsx index 38191305..fd74ec1b 100644 --- a/packages/components/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/components/src/components/Checkbox/Checkbox.stories.tsx @@ -21,6 +21,7 @@ export const Default: Story = { args: { children: 'Checkbox', disabled: false, + checked: true, }, } diff --git a/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx b/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx index 0420ce23..cd23846a 100644 --- a/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx +++ b/packages/components/src/components/CheckboxLayer/CheckboxLayer.stories.tsx @@ -8,11 +8,25 @@ const meta: Meta = { title: 'Devfive/CheckboxLayer', component: CheckboxLayer, decorators: [ - (Story) => ( -
- -
- ), + (Story, context) => { + const theme = + context.parameters.theme || context.globals.theme || 'default' + const isDark = theme === 'dark' + + return ( +
+ +
+ ) + }, ], argTypes: { onCheckboxChange: { action: 'checkbox changed' }, From 929035400103cf9f2a32ed509d82a36597a61a32 Mon Sep 17 00:00:00 2001 From: indianapoly Date: Wed, 17 Sep 2025 10:05:08 +0900 Subject: [PATCH 03/18] add: test --- .../__snapshots__/index.browser.test.tsx.snap | 30 ++ .../Checkbox/__tests__/index.browser.test.tsx | 445 ++++++++++++++++++ .../__snapshots__/index.browser.test.tsx.snap | 84 ++++ .../__tests__/index.browser.test.tsx | 226 +++++++++ 4 files changed, 785 insertions(+) create mode 100644 packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap create mode 100644 packages/components/src/components/Checkbox/__tests__/index.browser.test.tsx create mode 100644 packages/components/src/components/CheckboxLayer/__tests__/__snapshots__/index.browser.test.tsx.snap create mode 100644 packages/components/src/components/CheckboxLayer/__tests__/index.browser.test.tsx diff --git a/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap new file mode 100644 index 00000000..e6972535 --- /dev/null +++ b/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Checkbox > should render 1`] = ` +
+
+
+ +
+ +
+
+`; diff --git a/packages/components/src/components/Checkbox/__tests__/index.browser.test.tsx b/packages/components/src/components/Checkbox/__tests__/index.browser.test.tsx new file mode 100644 index 00000000..932a1ecc --- /dev/null +++ b/packages/components/src/components/Checkbox/__tests__/index.browser.test.tsx @@ -0,0 +1,445 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { Checkbox } from '..' + +// Mock getTheme function +vi.mock('@devup-ui/react', async (importOriginal) => { + const mod = await importOriginal() + return { + ...mod, + getTheme: vi.fn(() => 'light'), + } +}) + +const { getTheme } = await import('@devup-ui/react') + +describe('Checkbox', () => { + beforeEach(() => { + // Reset to light theme before each test + vi.mocked(getTheme).mockReturnValue('light') + }) + + afterEach(() => { + vi.clearAllMocks() + }) + it('should render', () => { + const { container } = render( + Checkbox, + ) + expect(container).toMatchSnapshot() + }) + + it('should render with text children', () => { + render(Checkbox Label) + expect(screen.getByText('Checkbox Label')).toBeInTheDocument() + }) + + it('should render with custom React node children', () => { + render( + + Custom Label + , + ) + expect(screen.getByTestId('custom-label')).toBeInTheDocument() + }) + + it('should render checked state with CheckIcon', () => { + const { container } = render( + + Checked Checkbox + , + ) + + const checkIcon = container.querySelector('svg') + expect(checkIcon).toBeInTheDocument() + expect(checkIcon).toHaveAttribute('width', '12') + expect(checkIcon).toHaveAttribute('height', '10') + }) + + it('should not render CheckIcon when unchecked', () => { + const { container } = render( + + Unchecked Checkbox + , + ) + + const checkIcon = container.querySelector('svg') + expect(checkIcon).not.toBeInTheDocument() + }) + + it('should handle disabled state', () => { + render( + + Disabled Checkbox + , + ) + + const checkbox = screen.getByRole('checkbox') + expect(checkbox).toBeDisabled() + }) + + it('should call onChange when clicked', () => { + const handleChange = vi.fn() + render( + + Clickable Checkbox + , + ) + + const checkbox = screen.getByRole('checkbox') + fireEvent.click(checkbox) + + expect(handleChange).toHaveBeenCalledWith(true) + }) + + it('should not call onChange when disabled and clicked', () => { + const handleChange = vi.fn() + render( + + Disabled Checkbox + , + ) + + const checkbox = screen.getByRole('checkbox') + fireEvent.click(checkbox) + + expect(handleChange).not.toHaveBeenCalled() + }) + + it('should have proper label association', () => { + render(Test Label) + + const checkbox = screen.getByRole('checkbox') + const label = screen.getByText('Test Label') + + expect(checkbox).toHaveAttribute('id', 'test-checkbox') + expect(label.closest('label')).toHaveAttribute('for', 'test-checkbox') + }) + + it('should render CheckIcon with disabled color when disabled and checked', () => { + const { container } = render( + + Disabled Checked Checkbox + , + ) + + const checkIcon = container.querySelector('svg path') + expect(checkIcon).toBeInTheDocument() + expect(checkIcon).toHaveAttribute('fill', '#D6D7DE') + }) + + it('should render CheckIcon with normal color when enabled and checked', () => { + const { container } = render( + + Enabled Checked Checkbox + , + ) + + const checkIcon = container.querySelector('svg path') + expect(checkIcon).toBeInTheDocument() + expect(checkIcon).toHaveAttribute('fill', '#FFF') + }) + + describe('Theme-based styling', () => { + it('should render CheckIcon with light theme disabled color when disabled and checked', () => { + vi.mocked(getTheme).mockReturnValue('light') + + const { container } = render( + + Light Theme Disabled Checked + , + ) + + const checkIcon = container.querySelector('svg path') + expect(checkIcon).toBeInTheDocument() + expect(checkIcon).toHaveAttribute('fill', '#D6D7DE') + }) + + it('should render CheckIcon with dark theme disabled color when disabled and checked', () => { + vi.mocked(getTheme).mockReturnValue('dark') + + const { container } = render( + + Dark Theme Disabled Checked + , + ) + + const checkIcon = container.querySelector('svg path') + expect(checkIcon).toBeInTheDocument() + expect(checkIcon).toHaveAttribute('fill', '#373737') + }) + + it('should render with light theme background colors', () => { + vi.mocked(getTheme).mockReturnValue('light') + + const { container } = render( + Light Theme Checkbox, + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + expect(checkbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + }) + + it('should render with dark theme background colors', () => { + vi.mocked(getTheme).mockReturnValue('dark') + + const { container } = render( + Dark Theme Checkbox, + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + expect(checkbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + }) + + it('should use theme for disabled state background', () => { + // Test light theme disabled + vi.mocked(getTheme).mockReturnValue('light') + + const { container: lightContainer } = render( + + Light Disabled + , + ) + + const lightCheckbox = lightContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(lightCheckbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + + // Test dark theme disabled + vi.mocked(getTheme).mockReturnValue('dark') + + const { container: darkContainer } = render( + + Dark Disabled + , + ) + + const darkCheckbox = darkContainer.querySelector('input[type="checkbox"]') + expect(darkCheckbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + }) + + it('should use theme for checked disabled state background', () => { + // Test light theme checked disabled + vi.mocked(getTheme).mockReturnValue('light') + + const { container: lightContainer } = render( + + Light Checked Disabled + , + ) + + const lightCheckbox = lightContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(lightCheckbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + + // Test dark theme checked disabled + vi.mocked(getTheme).mockReturnValue('dark') + + const { container: darkContainer } = render( + + Dark Checked Disabled + , + ) + + const darkCheckbox = darkContainer.querySelector('input[type="checkbox"]') + expect(darkCheckbox).toBeInTheDocument() + expect(getTheme).toHaveBeenCalled() + }) + }) + + it('should handle hover and active states', () => { + const { container } = render( + Interactive Checkbox, + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + expect(checkbox).toBeInTheDocument() + + if (checkbox) { + // Test active state + fireEvent.mouseDown(checkbox) + fireEvent.mouseUp(checkbox) + + // Test hover state + fireEvent.mouseEnter(checkbox) + fireEvent.mouseLeave(checkbox) + } + }) + + it('should handle checked state hover', () => { + const { container } = render( + + Checked Hover Checkbox + , + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + expect(checkbox).toBeInTheDocument() + + if (checkbox) { + // Test checked hover state + fireEvent.mouseEnter(checkbox) + fireEvent.mouseLeave(checkbox) + } + }) + + it('should render disabled label text with correct color', () => { + render( + + Disabled Label Text + , + ) + + const labelText = screen.getByText('Disabled Label Text') + expect(labelText).toBeInTheDocument() + }) + + it('should render enabled label text with correct color', () => { + render(Enabled Label Text) + + const labelText = screen.getByText('Enabled Label Text') + expect(labelText).toBeInTheDocument() + }) + + it('should handle all states correctly', () => { + // Test unchecked state + const { container: uncheckedContainer } = render( + Unchecked Checkbox, + ) + + const uncheckedCheckbox = uncheckedContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(uncheckedCheckbox).toBeInTheDocument() + expect(uncheckedCheckbox).not.toBeChecked() + + // Test checked state + const { container: checkedContainer } = render( + + Checked Checkbox + , + ) + + const checkedCheckbox = checkedContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(checkedCheckbox).toBeInTheDocument() + expect(checkedCheckbox).toBeChecked() + }) + + it('should handle disabled states', () => { + // Disabled unchecked + const { container: disabledContainer } = render( + + Disabled Checkbox + , + ) + + const disabledCheckbox = disabledContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(disabledCheckbox).toBeDisabled() + + // Disabled checked + const { container: disabledCheckedContainer } = render( + + Disabled Checked Checkbox + , + ) + + const disabledCheckedCheckbox = disabledCheckedContainer.querySelector( + 'input[type="checkbox"]', + ) + expect(disabledCheckedCheckbox).toBeDisabled() + expect(disabledCheckedCheckbox).toBeChecked() + }) + + it('should handle interactive states correctly', () => { + const { container } = render( + Interactive Checkbox, + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + if (checkbox) { + // Test active state + fireEvent.mouseDown(checkbox) + fireEvent.mouseUp(checkbox) + + // Test hover state + fireEvent.mouseEnter(checkbox) + fireEvent.mouseLeave(checkbox) + } + }) + + it('should handle checked hover state correctly', () => { + const { container } = render( + + Checked Hover Checkbox + , + ) + + const checkbox = container.querySelector('input[type="checkbox"]') + if (checkbox) { + // Test checked + hover state + fireEvent.mouseEnter(checkbox) + fireEvent.mouseLeave(checkbox) + } + }) + + it('should handle onChange conditional logic', () => { + const onChange = vi.fn() + + // Test !disabled && onChange?.() - both sides of && + + // Case 1: disabled=false, onChange exists (should call) + const { container: enabledContainer } = render( + + Enabled with onChange + , + ) + + const enabledCheckbox = enabledContainer.querySelector( + 'input[type="checkbox"]', + ) + if (enabledCheckbox) { + fireEvent.click(enabledCheckbox) + } + expect(onChange).toHaveBeenCalledWith(true) + + // Case 2: disabled=true, onChange exists (should not call) + onChange.mockClear() + const { container: disabledContainer } = render( + + Disabled with onChange + , + ) + + const disabledCheckbox = disabledContainer.querySelector( + 'input[type="checkbox"]', + ) + if (disabledCheckbox) { + fireEvent.click(disabledCheckbox) + } + expect(onChange).not.toHaveBeenCalled() + + // Case 3: disabled=false, no onChange (should not crash) + const { container: noOnChangeContainer } = render( + No onChange, + ) + + const noOnChangeCheckbox = noOnChangeContainer.querySelector( + 'input[type="checkbox"]', + ) + if (noOnChangeCheckbox) { + fireEvent.click(noOnChangeCheckbox) + } + // Should not crash - that's the test + }) +}) diff --git a/packages/components/src/components/CheckboxLayer/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/CheckboxLayer/__tests__/__snapshots__/index.browser.test.tsx.snap new file mode 100644 index 00000000..02293079 --- /dev/null +++ b/packages/components/src/components/CheckboxLayer/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -0,0 +1,84 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CheckboxLayer > should render 1`] = ` +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+`; diff --git a/packages/components/src/components/CheckboxLayer/__tests__/index.browser.test.tsx b/packages/components/src/components/CheckboxLayer/__tests__/index.browser.test.tsx new file mode 100644 index 00000000..d9799f83 --- /dev/null +++ b/packages/components/src/components/CheckboxLayer/__tests__/index.browser.test.tsx @@ -0,0 +1,226 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { CheckboxLayer } from '..' + +const mockCheckboxes = [ + { + id: 'checkbox1', + value: 'Option 1', + label: 'option-1', + }, + { + id: 'checkbox2', + value: 'Option 2', + label: 'option-2', + }, + { + id: 'checkbox3', + value: 'Option 3', + label: 'option-3', + disabled: true, + }, +] + +describe('CheckboxLayer', () => { + it('should render', () => { + const { container } = render( + , + ) + expect(container).toMatchSnapshot() + }) + + it('should render all checkboxes', () => { + render() + + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.getByText('Option 2')).toBeInTheDocument() + expect(screen.getByText('Option 3')).toBeInTheDocument() + }) + + it('should render with row direction', () => { + const { container } = render( + , + ) + + const flexContainer = container.firstChild + expect(flexContainer).toBeInTheDocument() + }) + + it('should render with column direction', () => { + const { container } = render( + , + ) + + const flexContainer = container.firstChild + expect(flexContainer).toBeInTheDocument() + }) + + it('should handle checkbox change event', () => { + const handleCheckboxChange = vi.fn() + render( + , + ) + + const firstCheckbox = screen.getByRole('checkbox', { name: /Option 1/i }) + fireEvent.click(firstCheckbox) + + expect(handleCheckboxChange).toHaveBeenCalledWith({ + id: 'checkbox1', + value: 'Option 1', + checked: true, + checkedValues: ['Option 1'], + }) + }) + + it('should handle multiple checkbox selections', () => { + const handleCheckboxChange = vi.fn() + render( + , + ) + + const firstCheckbox = screen.getByRole('checkbox', { name: /Option 1/i }) + const secondCheckbox = screen.getByRole('checkbox', { name: /Option 2/i }) + + fireEvent.click(firstCheckbox) + fireEvent.click(secondCheckbox) + + expect(handleCheckboxChange).toHaveBeenCalledWith({ + id: 'checkbox2', + value: 'Option 2', + checked: true, + checkedValues: ['Option 1', 'Option 2'], + }) + }) + + it('should handle checkbox unchecking', () => { + const handleCheckboxChange = vi.fn() + render( + , + ) + + const firstCheckbox = screen.getByRole('checkbox', { name: /Option 1/i }) + fireEvent.click(firstCheckbox) // uncheck + + expect(handleCheckboxChange).toHaveBeenCalledWith({ + id: 'checkbox1', + value: 'Option 1', + checked: false, + checkedValues: [], + }) + }) + + it('should render with default checked items', () => { + render( + , + ) + + const firstCheckbox = screen.getByRole('checkbox', { name: /Option 1/i }) + const secondCheckbox = screen.getByRole('checkbox', { name: /Option 2/i }) + const thirdCheckbox = screen.getByRole('checkbox', { name: /Option 3/i }) + + expect(firstCheckbox).toBeChecked() + expect(secondCheckbox).toBeChecked() + expect(thirdCheckbox).not.toBeChecked() + }) + + it('should handle disabled checkboxes', () => { + render() + + const disabledCheckbox = screen.getByRole('checkbox', { name: /Option 3/i }) + expect(disabledCheckbox).toBeDisabled() + }) + + it('should not trigger onChange for disabled checkboxes', () => { + const handleCheckboxChange = vi.fn() + render( + , + ) + + const disabledCheckbox = screen.getByRole('checkbox', { name: /Option 3/i }) + fireEvent.click(disabledCheckbox) + + expect(handleCheckboxChange).not.toHaveBeenCalled() + }) + + it('should render all checkboxes correctly', () => { + const { container } = render( + , + ) + + const checkboxes = container.querySelectorAll('input[type="checkbox"]') + expect(checkboxes).toHaveLength(3) + }) + + it('should render with custom gap', () => { + const { container } = render( + , + ) + + const flexContainer = container.firstChild + expect(flexContainer).toBeInTheDocument() + }) + + it('should generate unique labels for each checkbox', () => { + render() + + const checkbox1 = screen.getByRole('checkbox', { name: /Option 1/i }) + const checkbox2 = screen.getByRole('checkbox', { name: /Option 2/i }) + const checkbox3 = screen.getByRole('checkbox', { name: /Option 3/i }) + + expect(checkbox1).toHaveAttribute('id', 'checkbox1-option-1') + expect(checkbox2).toHaveAttribute('id', 'checkbox2-option-2') + expect(checkbox3).toHaveAttribute('id', 'checkbox3-option-3') + }) + + it('should handle complex checkbox values', () => { + const complexCheckboxes = [ + { + id: 'complex1', + value: Complex Value, + label: 'complex-option', + }, + ] + + const handleCheckboxChange = vi.fn() + render( + , + ) + + expect(screen.getByTestId('complex-value')).toBeInTheDocument() + + const checkbox = screen.getByRole('checkbox') + fireEvent.click(checkbox) + + expect(handleCheckboxChange).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'complex1', + checked: true, + }), + ) + }) +}) From bacc3b94dfee0520afa8367518fb2e3e3acf4eac Mon Sep 17 00:00:00 2001 From: indianapoly Date: Wed, 17 Sep 2025 10:05:32 +0900 Subject: [PATCH 04/18] remove variant, add darkmode --- .../src/components/Checkbox/index.tsx | 57 ++++++++++--------- .../src/components/CheckboxLayer/index.tsx | 3 - 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/components/src/components/Checkbox/index.tsx b/packages/components/src/components/Checkbox/index.tsx index e5935a9e..a59d32ea 100644 --- a/packages/components/src/components/Checkbox/index.tsx +++ b/packages/components/src/components/Checkbox/index.tsx @@ -1,4 +1,4 @@ -import { Box, css, Flex, Input, Text } from '@devup-ui/react' +import { Box, css, Flex, getTheme, Input, Text } from '@devup-ui/react' import { ComponentProps } from 'react' import { CheckIcon } from './CheckIcon' @@ -7,7 +7,6 @@ interface CheckboxProps extends Omit, 'type' | 'onChange'> { children: React.ReactNode onChange?: (checked: boolean) => void - variant?: 'primary' | 'default' label: string } @@ -16,10 +15,11 @@ export function Checkbox({ disabled, checked, onChange, - variant = 'primary', label, ...props }: CheckboxProps) { + const theme = getTheme() + return ( @@ -28,13 +28,11 @@ export function Checkbox({ disabled ? undefined : { - primary: { - bg: 'color-mix(in srgb, var(--primary) 20%, #FFF 80%)', - }, - default: { - bg: 'color-mix(in srgb, var(--primary) 30%, #FFF 70%)', - }, - }[variant] + bg: + theme === 'dark' + ? 'color-mix(in srgb, var(--primary) 30%, black 70%)' + : 'color-mix(in srgb, var(--primary) 20%, #FFF 80%)', + } } _checked={{ bg: '$primary', @@ -43,34 +41,31 @@ export function Checkbox({ ? undefined : { bg: - variant === 'primary' - ? 'color-mix(in srgb, var(--primary) 100%, #000 15%)' - : 'color-mix(in srgb, var(--primary) 100%, #FFF 15%)', + theme === 'dark' + ? 'color-mix(in srgb, var(--primary) 100%, #FFF 15%)' + : 'color-mix(in srgb, var(--primary) 100%, #000 15%)', }, _disabled: { - bg: '#F0F0F3', + bg: theme === 'dark' ? '#47474A' : '#F0F0F3', }, }} _disabled={{ - bg: '#F0F0F3', + bg: theme === 'dark' ? '#47474A' : '#F0F0F3', }} _hover={ disabled ? undefined : { - primary: { - bg: 'color-mix(in srgb, var(--primary) 10%, #FFF 90%)', - border: '1px solid var(--primary)', - }, - default: { - bg: 'color-mix(in srgb, var(--primary) 100%, #FFF 15%)', - border: '1px solid var(--primary)', - }, - }[variant] + bg: + theme === 'dark' + ? 'color-mix(in srgb, var(--primary) 20%, black 80%);' + : 'color-mix(in srgb, var(--primary) 10%, #FFF 90%)', + border: '1px solid var(--primary)', + } } accentColor="$primary" appearance="none" - bg="$contentBackground" + bg={theme === 'dark' ? '$inputBg' : '$contentBackground'} border="1px solid var(--border)" borderRadius="2px" boxSize="16px" @@ -93,17 +88,23 @@ export function Checkbox({ transform: 'translate(-50%, -50%)', pointerEvents: 'none', })} - color={disabled ? '#D6D7DE' : '#FFF'} + color={ + disabled ? (theme === 'dark' ? '#373737' : '#D6D7DE') : '#FFF' + } /> )} -