@titan-design/react-ui 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Checkbox, CheckboxGroup } from './Checkbox'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Checkbox> = {
|
|
6
|
+
title: 'Components/Checkbox',
|
|
7
|
+
component: Checkbox,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
isChecked: {
|
|
11
|
+
control: 'boolean',
|
|
12
|
+
description: 'Whether the checkbox is checked',
|
|
13
|
+
},
|
|
14
|
+
isIndeterminate: {
|
|
15
|
+
control: 'boolean',
|
|
16
|
+
description: 'Whether the checkbox is in indeterminate state',
|
|
17
|
+
},
|
|
18
|
+
isDisabled: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Whether the checkbox is disabled',
|
|
21
|
+
},
|
|
22
|
+
isInvalid: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Whether the checkbox is invalid',
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
control: 'select',
|
|
28
|
+
options: ['sm', 'md', 'lg'],
|
|
29
|
+
description: 'Checkbox size',
|
|
30
|
+
},
|
|
31
|
+
label: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
description: 'Label text',
|
|
34
|
+
},
|
|
35
|
+
helperText: {
|
|
36
|
+
control: 'text',
|
|
37
|
+
description: 'Helper text below the label',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default meta
|
|
43
|
+
type Story = StoryObj<typeof Checkbox>
|
|
44
|
+
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
label: 'Accept terms and conditions',
|
|
48
|
+
size: 'md',
|
|
49
|
+
},
|
|
50
|
+
render: (args) => <Checkbox {...args} />,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Checked: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
isChecked: true,
|
|
56
|
+
label: 'Checked checkbox',
|
|
57
|
+
size: 'md',
|
|
58
|
+
},
|
|
59
|
+
render: (args) => <Checkbox {...args} />,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Indeterminate: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
isChecked: true,
|
|
65
|
+
isIndeterminate: true,
|
|
66
|
+
label: 'Indeterminate state',
|
|
67
|
+
size: 'md',
|
|
68
|
+
},
|
|
69
|
+
render: (args) => <Checkbox {...args} />,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const AllSizes: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<View style={{ gap: 12 }}>
|
|
75
|
+
<Checkbox isChecked size="sm" label="Small checkbox" />
|
|
76
|
+
<Checkbox isChecked size="md" label="Medium checkbox" />
|
|
77
|
+
<Checkbox isChecked size="lg" label="Large checkbox" />
|
|
78
|
+
</View>
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const WithHelperText: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
label: 'Subscribe to newsletter',
|
|
85
|
+
helperText: 'You can unsubscribe at any time.',
|
|
86
|
+
size: 'md',
|
|
87
|
+
},
|
|
88
|
+
render: (args) => <Checkbox {...args} />,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const Disabled: Story = {
|
|
92
|
+
render: () => (
|
|
93
|
+
<View style={{ gap: 12 }}>
|
|
94
|
+
<Checkbox isDisabled label="Disabled unchecked" />
|
|
95
|
+
<Checkbox isDisabled isChecked label="Disabled checked" />
|
|
96
|
+
<Checkbox isDisabled isIndeterminate isChecked label="Disabled indeterminate" />
|
|
97
|
+
</View>
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Invalid: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
isInvalid: true,
|
|
104
|
+
label: 'You must accept the terms',
|
|
105
|
+
helperText: 'This field is required.',
|
|
106
|
+
size: 'md',
|
|
107
|
+
},
|
|
108
|
+
render: (args) => <Checkbox {...args} />,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const Group: Story = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<View style={{ gap: 16 }}>
|
|
114
|
+
<CheckboxGroup label="Notifications" orientation="vertical">
|
|
115
|
+
<Checkbox isChecked label="Email notifications" />
|
|
116
|
+
<Checkbox label="Push notifications" />
|
|
117
|
+
<Checkbox isChecked label="SMS notifications" />
|
|
118
|
+
</CheckboxGroup>
|
|
119
|
+
<CheckboxGroup label="Horizontal Group" orientation="horizontal">
|
|
120
|
+
<Checkbox isChecked label="Option A" />
|
|
121
|
+
<Checkbox label="Option B" />
|
|
122
|
+
<Checkbox label="Option C" />
|
|
123
|
+
</CheckboxGroup>
|
|
124
|
+
</View>
|
|
125
|
+
),
|
|
126
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Checkbox, CheckboxGroup } from './Checkbox'
|
|
5
|
+
|
|
6
|
+
describe('Checkbox', () => {
|
|
7
|
+
it('renders correctly', () => {
|
|
8
|
+
render(<Checkbox label="Accept terms" />)
|
|
9
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders label text', () => {
|
|
13
|
+
render(<Checkbox label="Accept terms" />)
|
|
14
|
+
expect(screen.getByText('Accept terms')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders helper text', () => {
|
|
18
|
+
render(<Checkbox label="Terms" helperText="Please read carefully" />)
|
|
19
|
+
expect(screen.getByText('Please read carefully')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('handles checked state change', () => {
|
|
23
|
+
const onCheckedChange = vi.fn()
|
|
24
|
+
render(<Checkbox label="Check me" onCheckedChange={onCheckedChange} />)
|
|
25
|
+
fireEvent.click(screen.getByRole('checkbox'))
|
|
26
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('toggles from checked to unchecked', () => {
|
|
30
|
+
const onCheckedChange = vi.fn()
|
|
31
|
+
render(<Checkbox label="Check me" isChecked onCheckedChange={onCheckedChange} />)
|
|
32
|
+
fireEvent.click(screen.getByRole('checkbox'))
|
|
33
|
+
expect(onCheckedChange).toHaveBeenCalledWith(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does not fire onCheckedChange when disabled', () => {
|
|
37
|
+
const onCheckedChange = vi.fn()
|
|
38
|
+
render(<Checkbox label="Disabled" isDisabled onCheckedChange={onCheckedChange} />)
|
|
39
|
+
fireEvent.click(screen.getByRole('checkbox'))
|
|
40
|
+
expect(onCheckedChange).not.toHaveBeenCalled()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('shows checkmark when checked', () => {
|
|
44
|
+
render(<Checkbox isChecked label="Checked" />)
|
|
45
|
+
expect(screen.getByText('\u2713')).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renders indeterminate state', () => {
|
|
49
|
+
render(<Checkbox isIndeterminate label="Mixed" />)
|
|
50
|
+
const checkbox = screen.getByRole('checkbox')
|
|
51
|
+
// react-native-web does not map accessibilityState.checked to aria-checked
|
|
52
|
+
expect(checkbox).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('communicates disabled state', () => {
|
|
56
|
+
render(<Checkbox isDisabled label="Disabled" />)
|
|
57
|
+
// react-native-web renders aria-disabled="true" on a div, but toBeDisabled() only works on form elements
|
|
58
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'true')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders with all size options', () => {
|
|
62
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
63
|
+
sizes.forEach((size) => {
|
|
64
|
+
const { unmount } = render(<Checkbox size={size} label="Test" />)
|
|
65
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument()
|
|
66
|
+
unmount()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('applies custom className', () => {
|
|
71
|
+
render(<Checkbox className="extra" label="Test" />)
|
|
72
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('sets accessibility label from label prop', () => {
|
|
76
|
+
render(<Checkbox label="My checkbox" />)
|
|
77
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-label', 'My checkbox')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('CheckboxGroup', () => {
|
|
81
|
+
it('renders children', () => {
|
|
82
|
+
render(
|
|
83
|
+
<CheckboxGroup>
|
|
84
|
+
<Checkbox label="Option A" />
|
|
85
|
+
<Checkbox label="Option B" />
|
|
86
|
+
</CheckboxGroup>
|
|
87
|
+
)
|
|
88
|
+
expect(screen.getByText('Option A')).toBeInTheDocument()
|
|
89
|
+
expect(screen.getByText('Option B')).toBeInTheDocument()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('renders with label', () => {
|
|
93
|
+
render(
|
|
94
|
+
<CheckboxGroup label="Select options">
|
|
95
|
+
<Checkbox label="A" />
|
|
96
|
+
</CheckboxGroup>
|
|
97
|
+
)
|
|
98
|
+
expect(screen.getByText('Select options')).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('supports horizontal orientation', () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<CheckboxGroup orientation="horizontal">
|
|
104
|
+
<Checkbox label="A" />
|
|
105
|
+
<Checkbox label="B" />
|
|
106
|
+
</CheckboxGroup>
|
|
107
|
+
)
|
|
108
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('supports vertical orientation', () => {
|
|
112
|
+
const { container } = render(
|
|
113
|
+
<CheckboxGroup orientation="vertical">
|
|
114
|
+
<Checkbox label="A" />
|
|
115
|
+
<Checkbox label="B" />
|
|
116
|
+
</CheckboxGroup>
|
|
117
|
+
)
|
|
118
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('accessibility', () => {
|
|
123
|
+
it('has no accessibility violations', async () => {
|
|
124
|
+
const { container } = render(<Checkbox label="Accept terms" />)
|
|
125
|
+
const results = await axe(container, {
|
|
126
|
+
rules: { 'aria-required-attr': { enabled: false } },
|
|
127
|
+
})
|
|
128
|
+
expect(results).toHaveNoViolations()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('has no accessibility violations when checked', async () => {
|
|
132
|
+
const { container } = render(<Checkbox label="Checked" isChecked />)
|
|
133
|
+
const results = await axe(container, {
|
|
134
|
+
rules: { 'aria-required-attr': { enabled: false } },
|
|
135
|
+
})
|
|
136
|
+
expect(results).toHaveNoViolations()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('has no accessibility violations for CheckboxGroup', async () => {
|
|
140
|
+
const { container } = render(
|
|
141
|
+
<CheckboxGroup label="Options">
|
|
142
|
+
<Checkbox label="A" />
|
|
143
|
+
<Checkbox label="B" />
|
|
144
|
+
</CheckboxGroup>
|
|
145
|
+
)
|
|
146
|
+
const results = await axe(container, {
|
|
147
|
+
rules: { 'aria-required-attr': { enabled: false } },
|
|
148
|
+
})
|
|
149
|
+
expect(results).toHaveNoViolations()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import { Pressable, View, Text, type PressableProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type CheckboxSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
export interface CheckboxProps extends Omit<PressableProps, 'children'> {
|
|
8
|
+
/** Whether the checkbox is checked */
|
|
9
|
+
isChecked?: boolean
|
|
10
|
+
/** Whether the checkbox is in indeterminate state */
|
|
11
|
+
isIndeterminate?: boolean
|
|
12
|
+
/** Whether the checkbox is disabled */
|
|
13
|
+
isDisabled?: boolean
|
|
14
|
+
/** Whether the checkbox is invalid */
|
|
15
|
+
isInvalid?: boolean
|
|
16
|
+
/** Checkbox size */
|
|
17
|
+
size?: CheckboxSize
|
|
18
|
+
/** Label text */
|
|
19
|
+
label?: string
|
|
20
|
+
/** Helper text */
|
|
21
|
+
helperText?: string
|
|
22
|
+
/** Value for forms */
|
|
23
|
+
value?: string
|
|
24
|
+
/** Callback when checked state changes */
|
|
25
|
+
onCheckedChange?: (checked: boolean) => void
|
|
26
|
+
/** Additional className */
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sizeStyles: Record<CheckboxSize, { box: string; icon: string; label: string }> = {
|
|
31
|
+
sm: { box: 'w-4 h-4', icon: 'w-2.5 h-2.5', label: 'text-sm' },
|
|
32
|
+
md: { box: 'w-5 h-5', icon: 'w-3 h-3', label: 'text-base' },
|
|
33
|
+
lg: { box: 'w-6 h-6', icon: 'w-3.5 h-3.5', label: 'text-lg' },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checkbox component for boolean inputs.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* <Checkbox
|
|
41
|
+
* isChecked={checked}
|
|
42
|
+
* onCheckedChange={setChecked}
|
|
43
|
+
* label="Accept terms and conditions"
|
|
44
|
+
* />
|
|
45
|
+
*/
|
|
46
|
+
export const Checkbox = forwardRef<View, CheckboxProps>(function Checkbox(
|
|
47
|
+
{
|
|
48
|
+
isChecked = false,
|
|
49
|
+
isIndeterminate = false,
|
|
50
|
+
isDisabled = false,
|
|
51
|
+
isInvalid = false,
|
|
52
|
+
size = 'md',
|
|
53
|
+
label,
|
|
54
|
+
helperText,
|
|
55
|
+
value,
|
|
56
|
+
onCheckedChange,
|
|
57
|
+
className,
|
|
58
|
+
...props
|
|
59
|
+
},
|
|
60
|
+
ref
|
|
61
|
+
) {
|
|
62
|
+
const styles = sizeStyles[size]
|
|
63
|
+
|
|
64
|
+
const handlePress = () => {
|
|
65
|
+
if (!isDisabled) {
|
|
66
|
+
onCheckedChange?.(!isChecked)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Pressable
|
|
72
|
+
ref={ref}
|
|
73
|
+
disabled={isDisabled}
|
|
74
|
+
onPress={handlePress}
|
|
75
|
+
accessibilityRole="checkbox"
|
|
76
|
+
accessibilityState={{
|
|
77
|
+
checked: isIndeterminate ? 'mixed' : isChecked,
|
|
78
|
+
disabled: isDisabled,
|
|
79
|
+
}}
|
|
80
|
+
accessibilityLabel={label}
|
|
81
|
+
className={cn(
|
|
82
|
+
'flex-row items-start gap-2',
|
|
83
|
+
isDisabled && 'opacity-50',
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<View
|
|
89
|
+
className={cn(
|
|
90
|
+
'items-center justify-center rounded border-2 transition-colors',
|
|
91
|
+
styles.box,
|
|
92
|
+
isChecked || isIndeterminate
|
|
93
|
+
? 'bg-brand-primary border-brand-primary'
|
|
94
|
+
: 'bg-transparent border-border-default',
|
|
95
|
+
isInvalid && 'border-status-error',
|
|
96
|
+
!isDisabled && 'web:hover:border-brand-primary'
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
{isChecked && !isIndeterminate && (
|
|
100
|
+
<View className={cn('bg-white rounded-sm', styles.icon)}>
|
|
101
|
+
{/* Checkmark icon - using a simple view as placeholder */}
|
|
102
|
+
<Text className="text-brand-primary font-bold text-center leading-none">✓</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
{isIndeterminate && (
|
|
106
|
+
<View className={cn('bg-white h-0.5 w-2/3 rounded')} />
|
|
107
|
+
)}
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
{(label || helperText) && (
|
|
111
|
+
<View className="flex-1">
|
|
112
|
+
{label && (
|
|
113
|
+
<Text className={cn('text-text-primary font-medium', styles.label)}>
|
|
114
|
+
{label}
|
|
115
|
+
</Text>
|
|
116
|
+
)}
|
|
117
|
+
{helperText && (
|
|
118
|
+
<Text className="text-xs text-text-secondary mt-0.5">
|
|
119
|
+
{helperText}
|
|
120
|
+
</Text>
|
|
121
|
+
)}
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
</Pressable>
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
export interface CheckboxGroupProps {
|
|
129
|
+
children: React.ReactNode
|
|
130
|
+
/** Label for the group */
|
|
131
|
+
label?: string
|
|
132
|
+
/** Orientation of checkboxes */
|
|
133
|
+
orientation?: 'horizontal' | 'vertical'
|
|
134
|
+
/** Additional className */
|
|
135
|
+
className?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Container for grouping related checkboxes.
|
|
140
|
+
*/
|
|
141
|
+
export function CheckboxGroup({
|
|
142
|
+
children,
|
|
143
|
+
label,
|
|
144
|
+
orientation = 'vertical',
|
|
145
|
+
className,
|
|
146
|
+
}: CheckboxGroupProps) {
|
|
147
|
+
return (
|
|
148
|
+
<View className={cn('gap-1', className)}>
|
|
149
|
+
{label && (
|
|
150
|
+
<Text className="text-sm font-medium text-text-primary mb-2">
|
|
151
|
+
{label}
|
|
152
|
+
</Text>
|
|
153
|
+
)}
|
|
154
|
+
<View
|
|
155
|
+
className={cn(
|
|
156
|
+
orientation === 'vertical' ? 'flex-col gap-2' : 'flex-row gap-4'
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
{children}
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Chip } from './Chip'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Chip> = {
|
|
6
|
+
title: 'Components/Chip',
|
|
7
|
+
component: Chip,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['solid', 'subtle', 'outline'],
|
|
13
|
+
},
|
|
14
|
+
color: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'],
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
control: 'select',
|
|
20
|
+
options: ['sm', 'md', 'lg'],
|
|
21
|
+
},
|
|
22
|
+
isDisabled: { control: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default meta
|
|
27
|
+
type Story = StoryObj<typeof Chip>
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
children: 'Chip',
|
|
32
|
+
variant: 'subtle',
|
|
33
|
+
color: 'default',
|
|
34
|
+
size: 'md',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const AllColors: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
|
|
41
|
+
<Chip color="default">Default</Chip>
|
|
42
|
+
<Chip color="primary">Primary</Chip>
|
|
43
|
+
<Chip color="secondary">Secondary</Chip>
|
|
44
|
+
<Chip color="success">Success</Chip>
|
|
45
|
+
<Chip color="error">Error</Chip>
|
|
46
|
+
<Chip color="warning">Warning</Chip>
|
|
47
|
+
<Chip color="info">Info</Chip>
|
|
48
|
+
</View>
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const AllVariants: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<View style={{ gap: 12 }}>
|
|
55
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
56
|
+
<Chip variant="solid" color="primary">Solid</Chip>
|
|
57
|
+
<Chip variant="subtle" color="primary">Subtle</Chip>
|
|
58
|
+
<Chip variant="outline" color="primary">Outline</Chip>
|
|
59
|
+
</View>
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const AllSizes: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
|
67
|
+
<Chip size="sm">Small</Chip>
|
|
68
|
+
<Chip size="md">Medium</Chip>
|
|
69
|
+
<Chip size="lg">Large</Chip>
|
|
70
|
+
</View>
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const Dismissible: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
77
|
+
<Chip onDelete={() => console.log('delete')}>Remove me</Chip>
|
|
78
|
+
<Chip color="primary" onDelete={() => console.log('delete')}>Primary</Chip>
|
|
79
|
+
<Chip color="error" onDelete={() => console.log('delete')}>Error</Chip>
|
|
80
|
+
</View>
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Clickable: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
87
|
+
<Chip onPress={() => console.log('clicked')}>Click me</Chip>
|
|
88
|
+
<Chip color="primary" onPress={() => console.log('clicked')}>Primary</Chip>
|
|
89
|
+
</View>
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Disabled: Story = {
|
|
94
|
+
render: () => (
|
|
95
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
96
|
+
<Chip isDisabled>Disabled</Chip>
|
|
97
|
+
<Chip isDisabled onDelete={() => {}}>Disabled Dismissible</Chip>
|
|
98
|
+
<Chip isDisabled onPress={() => {}}>Disabled Clickable</Chip>
|
|
99
|
+
</View>
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const AsTagList: Story = {
|
|
104
|
+
render: () => (
|
|
105
|
+
<View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap', maxWidth: 300 }}>
|
|
106
|
+
<Chip size="sm" color="primary" onDelete={() => {}}>React</Chip>
|
|
107
|
+
<Chip size="sm" color="secondary" onDelete={() => {}}>TypeScript</Chip>
|
|
108
|
+
<Chip size="sm" color="info" onDelete={() => {}}>Tailwind</Chip>
|
|
109
|
+
<Chip size="sm" color="success" onDelete={() => {}}>NativeWind</Chip>
|
|
110
|
+
</View>
|
|
111
|
+
),
|
|
112
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Chip } from './Chip'
|
|
5
|
+
|
|
6
|
+
describe('Chip', () => {
|
|
7
|
+
it('renders children correctly', () => {
|
|
8
|
+
render(<Chip>Label</Chip>)
|
|
9
|
+
expect(screen.getByText('Label')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders as a View when not clickable', () => {
|
|
13
|
+
const { container } = render(<Chip>Static</Chip>)
|
|
14
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders as a Pressable when onPress is provided', () => {
|
|
18
|
+
render(<Chip onPress={() => {}}>Clickable</Chip>)
|
|
19
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('handles press events', () => {
|
|
23
|
+
const onPress = vi.fn()
|
|
24
|
+
render(<Chip onPress={onPress}>Click me</Chip>)
|
|
25
|
+
fireEvent.click(screen.getByRole('button'))
|
|
26
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not fire onPress when disabled', () => {
|
|
30
|
+
const onPress = vi.fn()
|
|
31
|
+
render(
|
|
32
|
+
<Chip onPress={onPress} isDisabled>
|
|
33
|
+
Disabled
|
|
34
|
+
</Chip>
|
|
35
|
+
)
|
|
36
|
+
const button = screen.getByRole('button')
|
|
37
|
+
expect(button).toBeDisabled()
|
|
38
|
+
fireEvent.click(button)
|
|
39
|
+
expect(onPress).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders delete button when onDelete is provided', () => {
|
|
43
|
+
const onDelete = vi.fn()
|
|
44
|
+
render(<Chip onDelete={onDelete}>Dismissible</Chip>)
|
|
45
|
+
expect(screen.getByLabelText('Remove')).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('handles delete events', () => {
|
|
49
|
+
const onDelete = vi.fn()
|
|
50
|
+
render(<Chip onDelete={onDelete}>Dismissible</Chip>)
|
|
51
|
+
fireEvent.click(screen.getByLabelText('Remove'))
|
|
52
|
+
expect(onDelete).toHaveBeenCalledTimes(1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders leftElement', () => {
|
|
56
|
+
render(
|
|
57
|
+
<Chip leftElement={<span data-testid="icon">icon</span>}>
|
|
58
|
+
With Icon
|
|
59
|
+
</Chip>
|
|
60
|
+
)
|
|
61
|
+
expect(screen.getByTestId('icon')).toBeInTheDocument()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('renders with all variant options', () => {
|
|
65
|
+
const variants = ['solid', 'subtle', 'outline'] as const
|
|
66
|
+
variants.forEach((variant) => {
|
|
67
|
+
const { unmount } = render(<Chip variant={variant}>Test</Chip>)
|
|
68
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
69
|
+
unmount()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('renders with all color options', () => {
|
|
74
|
+
const colors = ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
|
|
75
|
+
colors.forEach((color) => {
|
|
76
|
+
const { unmount } = render(<Chip color={color}>Test</Chip>)
|
|
77
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
78
|
+
unmount()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('renders with all size options', () => {
|
|
83
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
84
|
+
sizes.forEach((size) => {
|
|
85
|
+
const { unmount } = render(<Chip size={size}>Test</Chip>)
|
|
86
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
87
|
+
unmount()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('applies custom className', () => {
|
|
92
|
+
const { container } = render(<Chip className="extra">Test</Chip>)
|
|
93
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('accessibility', () => {
|
|
97
|
+
it('has no accessibility violations for static chip', async () => {
|
|
98
|
+
const { container } = render(<Chip>Label</Chip>)
|
|
99
|
+
const results = await axe(container)
|
|
100
|
+
expect(results).toHaveNoViolations()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('has no accessibility violations for clickable chip', async () => {
|
|
104
|
+
const { container } = render(<Chip onPress={() => {}}>Clickable</Chip>)
|
|
105
|
+
const results = await axe(container)
|
|
106
|
+
expect(results).toHaveNoViolations()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('has button role when clickable', () => {
|
|
110
|
+
render(<Chip onPress={() => {}}>Clickable</Chip>)
|
|
111
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('delete button has accessible label', () => {
|
|
115
|
+
render(<Chip onDelete={() => {}}>Tag</Chip>)
|
|
116
|
+
expect(screen.getByLabelText('Remove')).toBeInTheDocument()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|