Skip to content

Commit

Permalink
feat: update checkbox component and dependencies
Browse files Browse the repository at this point in the history
- Add checkbox dependencies
- Fix checkbox element positioning
  • Loading branch information
jiji-hoon96 committed Jan 20, 2025
1 parent fba28cc commit a6a2a04
Show file tree
Hide file tree
Showing 14 changed files with 788 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/checkbox/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StorybookConfig } from '@storybook/react-vite';

export default {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
} satisfies StorybookConfig;
16 changes: 16 additions & 0 deletions packages/checkbox/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Preview } from '@storybook/react';
import 'sanitize.css';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;
1 change: 1 addition & 0 deletions packages/checkbox/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.module.css';
71 changes: 71 additions & 0 deletions packages/checkbox/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@sipe-team/checkbox",
"description": "Checkbox for Sipe Design System",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/sipe-team/3-2_side"
},
"type": "module",
"exports": "./src/index.ts",
"files": ["dist"],
"scripts": {
"build": "tsup",
"build:storybook": "storybook build",
"dev:storybook": "storybook dev -p 6006",
"lint:biome": "pnpm exec biome lint",
"lint:eslint": "pnpm exec eslint --flag unstable_ts_config",
"test": "vitest",
"typecheck": "tsc",
"prepack": "pnpm run build"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@sipe-team/tokens": "workspace:^",
"@sipe-team/typography": "workspace:^",
"clsx": "^2.1.1",
"lucide-react": "^0.344.0"
},
"devDependencies": {
"@storybook/addon-essentials": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-links": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/react": "catalog:",
"@storybook/react-vite": "catalog:",
"@storybook/test": "catalog:",
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.12",
"happy-dom": "catalog:",
"react": "^18.3.1",
"storybook": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"react": ">= 18"
},
"publishConfig": {
"access": "public",
"registry": "https://npm.pkg.github.com",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./styles.css": "./dist/index.css"
}
},
"sideEffects": false
}
52 changes: 52 additions & 0 deletions packages/checkbox/src/Checkbox.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.root {
all: unset;
display: flex;
align-items: center;
gap: var(--padding);
}

.label {
display: flex;
align-items: center;
gap: var(--padding);
margin-left: var(--margin);
}

.checkbox {
all: unset;
width: var(--size);
height: var(--size);
border-radius: 4px;
border: 2px solid var(--border-color);
background-color: var(--background-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}

.checkbox[data-disabled] {
cursor: not-allowed;
opacity: 0.4;
}

.checkbox[data-state="checked"] {
border-color: var(--checked-border-color);
background-color: var(--checked-background-color);
}

.indicator {
color: var(--indicator-color);
height: var(--indicator-size);
width: var(--indicator-size);
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
}

.group {
display: flex;
flex-direction: column;
gap: 8px;
}
102 changes: 102 additions & 0 deletions packages/checkbox/src/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Checkbox, CheckboxGroup } from './Checkbox';

const meta = {
title: 'Components/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered',
},
argTypes: {
size: {
description: '체크박스의 크기를 지정합니다',
options: ['sm', 'md', 'lg'],
control: { type: 'radio' },
},
disabled: {
description: '체크박스의 비활성화 상태를 지정합니다',
control: { type: 'boolean' },
},
checked: {
description: '체크박스의 선택 상태를 지정합니다',
control: { type: 'boolean' },
},
label: {
description: '체크박스의 레이블을 지정합니다',
control: 'text',
},
},
} satisfies Meta<typeof Checkbox>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic: Story = {
args: {
label: '체크박스',
size: 'md',
},
};

export const Sizes: Story = {
args: {
label: '체크박스',
},
render: (args) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Checkbox {...args} size="sm" label="Small" />
<Checkbox {...args} size="md" label="Medium (Default)" />
<Checkbox {...args} size="lg" label="Large" />
</div>
),
};

export const States: Story = {
args: {
label: '체크박스',
},
render: (args) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Checkbox {...args} label="Unchecked (Default)" />
<Checkbox {...args} label="Checked" defaultChecked />
<Checkbox {...args} label="Disabled" disabled />
<Checkbox {...args} label="Disabled & Checked" disabled defaultChecked />
</div>
),
};

export const Controlled: Story = {
args: {
label: 'Controlled Checkbox',
},
render: (args) => {
const [checked, setChecked] = useState<boolean | 'indeterminate'>(false);

return (
<Checkbox
{...args}
checked={checked}
onCheckedChange={(state) => setChecked(state)}
/>
);
},
};

export const Group: Story = {
args: {
size: 'md',
},
render: (args) => {
const [selected, setSelected] = useState<string[]>([]);

return (
<CheckboxGroup value={selected} onChange={setSelected}>
<Checkbox {...args} value="apple" label="사과" />
<Checkbox {...args} value="banana" label="바나나" />
<Checkbox {...args} value="orange" label="오렌지" />
</CheckboxGroup>
);
},
};
97 changes: 97 additions & 0 deletions packages/checkbox/src/Checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import { useState } from 'react';
import { Checkbox, CheckboxGroup } from './Checkbox';

test('체크박스를 클릭하면 상태가 변경된다.', async () => {
const user = userEvent.setup();
render(<Checkbox label="테스트" />);

const checkbox = screen.getByRole('checkbox', { name: '테스트' });
expect(checkbox).not.toBeChecked();

await user.click(checkbox);
expect(checkbox).toBeChecked();
});

test('disabled 상태의 체크박스는 클릭할 수 없다.', async () => {
const user = userEvent.setup();
render(<Checkbox disabled label="테스트" />);

const checkbox = screen.getByRole('checkbox', { name: '테스트' });
expect(checkbox).toBeDisabled();

await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});

test('size를 주입하지 않으면 기본값 md로 설정된다.', () => {
render(<Checkbox label="테스트" />);

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveStyle({ width: '20px', height: '20px' });
});

test.each([
{ size: 'sm', expected: '16px' },
{ size: 'md', expected: '20px' },
{ size: 'lg', expected: '24px' },
] as const)('size가 $size일 때 체크박스 크기가 $expected로 설정된다.', ({ size, expected }) => {
render(<Checkbox size={size} label="테스트" />);

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveStyle({ width: expected, height: expected });
});

test('체크박스 그룹에서 여러 항목을 선택할 수 있다.', async () => {
const user = userEvent.setup();

const TestComponent = () => {
const [selected, setSelected] = useState<string[]>([]);
return (
<CheckboxGroup value={selected} onChange={setSelected}>
<Checkbox value="1" label="항목 1" />
<Checkbox value="2" label="항목 2" />
<Checkbox value="3" label="항목 3" />
</CheckboxGroup>
);
};

render(<TestComponent />);

const checkbox1 = screen.getByRole('checkbox', { name: '항목 1' });
const checkbox2 = screen.getByRole('checkbox', { name: '항목 2' });

await user.click(checkbox1);
expect(checkbox1).toBeChecked();
expect(checkbox2).not.toBeChecked();

await user.click(checkbox2);
expect(checkbox1).toBeChecked();
expect(checkbox2).toBeChecked();

await user.click(checkbox1);
expect(checkbox1).not.toBeChecked();
expect(checkbox2).toBeChecked();
});

test('체크박스 그룹에서 value prop으로 선택된 항목을 제어할 수 있다.', async () => {
const selectedValues = ['1', '3'];

render(
<CheckboxGroup value={selectedValues}>
<Checkbox value="1" label="항목 1" />
<Checkbox value="2" label="항목 2" />
<Checkbox value="3" label="항목 3" />
</CheckboxGroup>
);

const checkbox1 = screen.getByRole('checkbox', { name: '항목 1' });
const checkbox2 = screen.getByRole('checkbox', { name: '항목 2' });
const checkbox3 = screen.getByRole('checkbox', { name: '항목 3' });

expect(checkbox1).toHaveAttribute('data-state', 'checked');
expect(checkbox2).toHaveAttribute('data-state', 'unchecked');
expect(checkbox3).toHaveAttribute('data-state', 'checked');
});
Loading

0 comments on commit a6a2a04

Please sign in to comment.