@duro-app/ui 0.23.0 → 0.25.0
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/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +44 -0
- package/dist/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
- package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
- package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
- package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
- package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
- package/dist/components/Combobox/styles.css.d.ts +112 -0
- package/dist/components/Combobox/styles.css.d.ts.map +1 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
- package/dist/components/Field/Field.d.ts +4 -0
- package/dist/components/Field/Field.d.ts.map +1 -1
- package/dist/components/Field/Field.stories.d.ts +3 -0
- package/dist/components/Field/Field.stories.d.ts.map +1 -1
- package/dist/components/Field/FieldContext.d.ts +5 -0
- package/dist/components/Field/FieldContext.d.ts.map +1 -1
- package/dist/components/Field/styles.css.d.ts +26 -0
- package/dist/components/Field/styles.css.d.ts.map +1 -1
- package/dist/components/Form/Form.d.ts +7 -2
- package/dist/components/Form/Form.d.ts.map +1 -1
- package/dist/components/Form/Form.stories.d.ts +6 -0
- package/dist/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/components/Form/FormContext.d.ts +9 -2
- package/dist/components/Form/FormContext.d.ts.map +1 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.meta.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -1
- package/dist/components/Table/ColumnFilter.d.ts +8 -0
- package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
- package/dist/components/Table/Pagination.d.ts +7 -0
- package/dist/components/Table/Pagination.d.ts.map +1 -0
- package/dist/components/Table/SortIndicator.d.ts +16 -0
- package/dist/components/Table/SortIndicator.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +7 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +4 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +11 -3
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Table/useDataTable.d.ts +32 -0
- package/dist/components/Table/useDataTable.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2863 -2026
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +32 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
- package/src/components/ButtonGroup/styles.css.ts +28 -0
- package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
- package/src/components/Combobox/Combobox.meta.ts +35 -0
- package/src/components/Combobox/Combobox.stories.tsx +119 -0
- package/src/components/Combobox/Combobox.tsx +247 -0
- package/src/components/Combobox/ComboboxContext.ts +26 -0
- package/src/components/Combobox/styles.css.ts +119 -0
- package/src/components/Combobox/useComboboxRoot.ts +235 -0
- package/src/components/Field/Field.stories.tsx +60 -0
- package/src/components/Field/Field.tsx +97 -9
- package/src/components/Field/FieldContext.ts +5 -0
- package/src/components/Field/styles.css.ts +26 -0
- package/src/components/Form/Form.stories.tsx +275 -0
- package/src/components/Form/Form.tsx +73 -7
- package/src/components/Form/FormContext.ts +10 -1
- package/src/components/Input/Input.tsx +2 -1
- package/src/components/List/List.meta.ts +8 -2
- package/src/components/List/List.stories.tsx +35 -15
- package/src/components/List/List.tsx +1 -5
- package/src/components/RadioGroup/RadioGroup.meta.ts +4 -1
- package/src/components/RadioGroup/RadioGroup.tsx +1 -4
- package/src/components/Table/ColumnFilter.tsx +46 -0
- package/src/components/Table/Pagination.tsx +82 -0
- package/src/components/Table/SortIndicator.tsx +53 -0
- package/src/components/Table/Table.stories.tsx +397 -0
- package/src/components/Table/Table.tsx +58 -24
- package/src/components/Table/styles.css.ts +16 -6
- package/src/components/Table/useDataTable.ts +99 -0
- package/src/components/Textarea/Textarea.tsx +2 -1
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +5 -0
- package/src/strict.css +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@hookform/resolvers": "^5.0.0",
|
|
37
|
+
"@tanstack/react-table": ">=8.0.0",
|
|
37
38
|
"effect": "^3.0.0",
|
|
38
39
|
"react": "^19.0.0",
|
|
39
40
|
"react-dom": "^19.0.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@duro-app/tokens": "^0.
|
|
56
|
+
"@duro-app/tokens": "^0.25.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -85,7 +85,7 @@ function SelectableTable({
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<>
|
|
88
|
-
<Table.Root
|
|
88
|
+
<Table.Root>
|
|
89
89
|
<Table.Header>
|
|
90
90
|
<Table.Row>
|
|
91
91
|
<Table.HeaderCell aria-label="Select">
|
|
@@ -172,7 +172,7 @@ export const Emphasized: Story = {
|
|
|
172
172
|
}
|
|
173
173
|
return (
|
|
174
174
|
<>
|
|
175
|
-
<Table.Root
|
|
175
|
+
<Table.Root>
|
|
176
176
|
<Table.Header>
|
|
177
177
|
<Table.Row>
|
|
178
178
|
<Table.HeaderCell aria-label="Select">
|
|
@@ -230,7 +230,7 @@ export const Emphasized: Story = {
|
|
|
230
230
|
export const CustomLabel: Story = {
|
|
231
231
|
render: () => (
|
|
232
232
|
<>
|
|
233
|
-
<Table.Root
|
|
233
|
+
<Table.Root>
|
|
234
234
|
<Table.Header>
|
|
235
235
|
<Table.Row>
|
|
236
236
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Groups related buttons together with consistent spacing and layout. Supports horizontal and vertical orientation with alignment control.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Dialog footers with Save/Cancel actions',
|
|
8
|
+
'Toolbar-style groupings of related actions',
|
|
9
|
+
'Form submission areas with multiple actions',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Single button — just use Button directly',
|
|
13
|
+
'Wrapping tags or badges — use Cluster instead',
|
|
14
|
+
'Navigation items — use Inline or SideNav',
|
|
15
|
+
],
|
|
16
|
+
relatedTo: [
|
|
17
|
+
{
|
|
18
|
+
component: 'Inline',
|
|
19
|
+
relationship:
|
|
20
|
+
'Inline is a general-purpose horizontal layout; ButtonGroup adds role="group" and disabled support',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
component: 'ActionBar',
|
|
24
|
+
relationship:
|
|
25
|
+
'ActionBar is a floating bulk-selection toolbar; ButtonGroup is a static grouping',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
example: `<ButtonGroup align="end" gap="sm">
|
|
29
|
+
<Button variant="secondary">Cancel</Button>
|
|
30
|
+
<Button variant="primary">Save</Button>
|
|
31
|
+
</ButtonGroup>`,
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect} from 'storybook/test'
|
|
3
|
+
import {ButtonGroup} from './ButtonGroup'
|
|
4
|
+
import {Button} from '../Button/Button'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ButtonGroup> = {
|
|
7
|
+
title: 'Components/ButtonGroup',
|
|
8
|
+
component: ButtonGroup,
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: {control: 'select', options: ['horizontal', 'vertical']},
|
|
11
|
+
align: {control: 'select', options: ['start', 'end', 'center']},
|
|
12
|
+
gap: {control: 'select', options: ['xs', 'sm', 'md']},
|
|
13
|
+
disabled: {control: 'boolean'},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
type Story = StoryObj<typeof ButtonGroup>
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<ButtonGroup>
|
|
23
|
+
<Button variant="primary">Save</Button>
|
|
24
|
+
<Button variant="secondary">Cancel</Button>
|
|
25
|
+
<Button variant="danger">Delete</Button>
|
|
26
|
+
</ButtonGroup>
|
|
27
|
+
),
|
|
28
|
+
play: async ({canvas}) => {
|
|
29
|
+
const group = canvas.getByRole('group')
|
|
30
|
+
await expect(group).toBeInTheDocument()
|
|
31
|
+
const buttons = canvas.getAllByRole('button')
|
|
32
|
+
await expect(buttons.length).toBe(3)
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Vertical: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<ButtonGroup orientation="vertical">
|
|
39
|
+
<Button variant="primary">Save</Button>
|
|
40
|
+
<Button variant="secondary">Cancel</Button>
|
|
41
|
+
<Button variant="danger">Delete</Button>
|
|
42
|
+
</ButtonGroup>
|
|
43
|
+
),
|
|
44
|
+
play: async ({canvas}) => {
|
|
45
|
+
const group = canvas.getByRole('group')
|
|
46
|
+
await expect(group).toBeInTheDocument()
|
|
47
|
+
const buttons = canvas.getAllByRole('button')
|
|
48
|
+
await expect(buttons.length).toBe(3)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const EndAligned: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<ButtonGroup align="end">
|
|
55
|
+
<Button variant="secondary">Cancel</Button>
|
|
56
|
+
<Button variant="primary">Save</Button>
|
|
57
|
+
</ButtonGroup>
|
|
58
|
+
),
|
|
59
|
+
play: async ({canvas}) => {
|
|
60
|
+
const group = canvas.getByRole('group')
|
|
61
|
+
await expect(group).toBeInTheDocument()
|
|
62
|
+
const buttons = canvas.getAllByRole('button')
|
|
63
|
+
await expect(buttons.length).toBe(2)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Disabled: Story = {
|
|
68
|
+
render: () => (
|
|
69
|
+
<ButtonGroup disabled>
|
|
70
|
+
<Button variant="primary">Save</Button>
|
|
71
|
+
<Button variant="secondary">Cancel</Button>
|
|
72
|
+
<Button variant="danger">Delete</Button>
|
|
73
|
+
</ButtonGroup>
|
|
74
|
+
),
|
|
75
|
+
play: async ({canvas}) => {
|
|
76
|
+
const group = canvas.getByRole('group')
|
|
77
|
+
await expect(group).toBeInTheDocument()
|
|
78
|
+
await expect(group).toHaveStyle({opacity: '0.5', pointerEvents: 'none'})
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const WithGaps: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 24}}>
|
|
85
|
+
<ButtonGroup gap="xs">
|
|
86
|
+
<Button variant="primary">XS Gap</Button>
|
|
87
|
+
<Button variant="secondary">XS Gap</Button>
|
|
88
|
+
</ButtonGroup>
|
|
89
|
+
<ButtonGroup gap="sm">
|
|
90
|
+
<Button variant="primary">SM Gap</Button>
|
|
91
|
+
<Button variant="secondary">SM Gap</Button>
|
|
92
|
+
</ButtonGroup>
|
|
93
|
+
<ButtonGroup gap="md">
|
|
94
|
+
<Button variant="primary">MD Gap</Button>
|
|
95
|
+
<Button variant="secondary">MD Gap</Button>
|
|
96
|
+
</ButtonGroup>
|
|
97
|
+
</div>
|
|
98
|
+
),
|
|
99
|
+
play: async ({canvas}) => {
|
|
100
|
+
const groups = canvas.getAllByRole('group')
|
|
101
|
+
await expect(groups.length).toBe(3)
|
|
102
|
+
},
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
export interface ButtonGroupProps {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
/** Layout direction. Default: 'horizontal' */
|
|
8
|
+
orientation?: 'horizontal' | 'vertical'
|
|
9
|
+
/** Alignment within container. Default: 'start' */
|
|
10
|
+
align?: 'start' | 'end' | 'center'
|
|
11
|
+
/** Disable all buttons in group */
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
/** Gap between buttons. Default: 'sm' */
|
|
14
|
+
gap?: 'xs' | 'sm' | 'md'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const gapMap = {
|
|
18
|
+
xs: styles.gapXs,
|
|
19
|
+
sm: styles.gapSm,
|
|
20
|
+
md: styles.gapMd,
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
const horizontalAlignMap = {
|
|
24
|
+
start: styles.alignStart,
|
|
25
|
+
center: styles.alignCenter,
|
|
26
|
+
end: styles.alignEnd,
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
const verticalAlignMap = {
|
|
30
|
+
start: styles.verticalAlignStart,
|
|
31
|
+
center: styles.verticalAlignCenter,
|
|
32
|
+
end: styles.verticalAlignEnd,
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export function ButtonGroup({
|
|
36
|
+
children,
|
|
37
|
+
orientation = 'horizontal',
|
|
38
|
+
align = 'start',
|
|
39
|
+
disabled = false,
|
|
40
|
+
gap = 'sm',
|
|
41
|
+
}: ButtonGroupProps) {
|
|
42
|
+
const isVertical = orientation === 'vertical'
|
|
43
|
+
const alignStyle = isVertical ? verticalAlignMap[align] : horizontalAlignMap[align]
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<html.div
|
|
47
|
+
role="group"
|
|
48
|
+
style={[
|
|
49
|
+
styles.base,
|
|
50
|
+
isVertical ? styles.vertical : styles.horizontal,
|
|
51
|
+
gapMap[gap],
|
|
52
|
+
alignStyle,
|
|
53
|
+
disabled && styles.disabled,
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</html.div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
3
|
+
|
|
4
|
+
export const styles = css.create({
|
|
5
|
+
base: {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
},
|
|
8
|
+
horizontal: {
|
|
9
|
+
flexDirection: 'row',
|
|
10
|
+
flexWrap: 'wrap',
|
|
11
|
+
},
|
|
12
|
+
vertical: {
|
|
13
|
+
flexDirection: 'column',
|
|
14
|
+
},
|
|
15
|
+
alignStart: {justifyContent: 'flex-start'},
|
|
16
|
+
alignCenter: {justifyContent: 'center'},
|
|
17
|
+
alignEnd: {justifyContent: 'flex-end'},
|
|
18
|
+
verticalAlignStart: {alignItems: 'flex-start'},
|
|
19
|
+
verticalAlignCenter: {alignItems: 'center'},
|
|
20
|
+
verticalAlignEnd: {alignItems: 'flex-end'},
|
|
21
|
+
gapXs: {gap: spacing.xs},
|
|
22
|
+
gapSm: {gap: spacing.sm},
|
|
23
|
+
gapMd: {gap: spacing.md},
|
|
24
|
+
disabled: {
|
|
25
|
+
opacity: 0.5,
|
|
26
|
+
pointerEvents: 'none',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
@@ -13,7 +13,10 @@ export const meta: ComponentMeta = {
|
|
|
13
13
|
'Many options (10+) — consider a filterable list or Select with multi-select',
|
|
14
14
|
],
|
|
15
15
|
relatedTo: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
component: 'RadioGroup',
|
|
18
|
+
relationship: 'RadioGroup for single-select; CheckboxGroup for multi-select',
|
|
19
|
+
},
|
|
17
20
|
{component: 'Checkbox', relationship: 'CheckboxGroup.Item wraps Checkbox internally'},
|
|
18
21
|
{component: 'ToggleGroup', relationship: 'ToggleGroup for toolbar-style multi-select'},
|
|
19
22
|
],
|
|
@@ -46,9 +46,7 @@ function Root({
|
|
|
46
46
|
const onToggle = useCallback(
|
|
47
47
|
(itemValue: string, checked: boolean) => {
|
|
48
48
|
if (disabled) return
|
|
49
|
-
const next = checked
|
|
50
|
-
? [...value, itemValue]
|
|
51
|
-
: value.filter((v) => v !== itemValue)
|
|
49
|
+
const next = checked ? [...value, itemValue] : value.filter((v) => v !== itemValue)
|
|
52
50
|
setValue(next)
|
|
53
51
|
},
|
|
54
52
|
[disabled, value, setValue],
|
|
@@ -88,12 +86,7 @@ function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
|
|
|
88
86
|
)
|
|
89
87
|
|
|
90
88
|
return (
|
|
91
|
-
<Checkbox
|
|
92
|
-
value={value}
|
|
93
|
-
checked={isChecked}
|
|
94
|
-
disabled={isDisabled}
|
|
95
|
-
onChange={handleChange}
|
|
96
|
-
>
|
|
89
|
+
<Checkbox value={value} checked={isChecked} disabled={isDisabled} onChange={handleChange}>
|
|
97
90
|
{children}
|
|
98
91
|
</Checkbox>
|
|
99
92
|
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Searchable dropdown for selecting a value from a filterable list. Combines a text input with a popup listbox. Compound component — Root is required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Large option lists where users benefit from typing to filter (e.g. country, city, user picker)',
|
|
8
|
+
'Autocomplete or typeahead scenarios',
|
|
9
|
+
'Table column filters with known categorical values',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Small lists (fewer than 7 options) — use Select instead',
|
|
13
|
+
'Free-form text entry — use Input',
|
|
14
|
+
'Multiple selections — use CheckboxGroup or ToggleGroup',
|
|
15
|
+
],
|
|
16
|
+
anatomy: {
|
|
17
|
+
required: ['Root', 'Input', 'Popup', 'Item'],
|
|
18
|
+
optional: ['Empty'],
|
|
19
|
+
},
|
|
20
|
+
relatedTo: [
|
|
21
|
+
{component: 'Select', relationship: 'Select for short lists without search; Combobox for long filterable lists'},
|
|
22
|
+
{component: 'Input', relationship: 'Input for free-form text; Combobox for constrained selection'},
|
|
23
|
+
{component: 'Menu', relationship: 'Menu triggers actions; Combobox picks a value'},
|
|
24
|
+
],
|
|
25
|
+
example: `<Combobox.Root name="country" defaultValue="us" initialLabels={{us: 'United States'}}>
|
|
26
|
+
<Combobox.Input placeholder="Search countries…" />
|
|
27
|
+
<Combobox.Popup>
|
|
28
|
+
<Combobox.Item value="us">United States</Combobox.Item>
|
|
29
|
+
<Combobox.Item value="gb">United Kingdom</Combobox.Item>
|
|
30
|
+
<Combobox.Item value="fr">France</Combobox.Item>
|
|
31
|
+
<Combobox.Item value="de">Germany</Combobox.Item>
|
|
32
|
+
<Combobox.Empty>No countries found</Combobox.Empty>
|
|
33
|
+
</Combobox.Popup>
|
|
34
|
+
</Combobox.Root>`,
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect} from 'storybook/test'
|
|
4
|
+
import {Combobox} from './Combobox'
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Components/Combobox',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
type Story = StoryObj
|
|
12
|
+
|
|
13
|
+
const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape', 'Honeydew']
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<Combobox.Root>
|
|
18
|
+
<Combobox.Input placeholder="Search fruit...">
|
|
19
|
+
<Combobox.Trigger />
|
|
20
|
+
</Combobox.Input>
|
|
21
|
+
<Combobox.Popup>
|
|
22
|
+
{fruits.map((fruit) => (
|
|
23
|
+
<Combobox.Item key={fruit} value={fruit.toLowerCase()}>
|
|
24
|
+
<Combobox.ItemText>{fruit}</Combobox.ItemText>
|
|
25
|
+
</Combobox.Item>
|
|
26
|
+
))}
|
|
27
|
+
<Combobox.Empty>No fruits found</Combobox.Empty>
|
|
28
|
+
</Combobox.Popup>
|
|
29
|
+
</Combobox.Root>
|
|
30
|
+
),
|
|
31
|
+
play: async ({canvas}) => {
|
|
32
|
+
const input = canvas.getByRole('combobox')
|
|
33
|
+
await expect(input).toBeInTheDocument()
|
|
34
|
+
await expect(input).toHaveAttribute('aria-expanded', 'false')
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const WithDefaultValue: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<Combobox.Root defaultValue="cherry" initialLabels={{cherry: 'Cherry'}}>
|
|
41
|
+
<Combobox.Input placeholder="Search fruit...">
|
|
42
|
+
<Combobox.Trigger />
|
|
43
|
+
</Combobox.Input>
|
|
44
|
+
<Combobox.Popup>
|
|
45
|
+
{fruits.map((fruit) => (
|
|
46
|
+
<Combobox.Item key={fruit} value={fruit.toLowerCase()}>
|
|
47
|
+
<Combobox.ItemText>{fruit}</Combobox.ItemText>
|
|
48
|
+
</Combobox.Item>
|
|
49
|
+
))}
|
|
50
|
+
<Combobox.Empty>No fruits found</Combobox.Empty>
|
|
51
|
+
</Combobox.Popup>
|
|
52
|
+
</Combobox.Root>
|
|
53
|
+
),
|
|
54
|
+
play: async ({canvas}) => {
|
|
55
|
+
const input = canvas.getByRole('combobox')
|
|
56
|
+
await expect(input).toHaveValue('Cherry')
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ControlledDemo() {
|
|
61
|
+
const [value, setValue] = useState<string | null>(null)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
|
|
65
|
+
<Combobox.Root value={value} onValueChange={setValue}>
|
|
66
|
+
<Combobox.Input placeholder="Pick a role...">
|
|
67
|
+
<Combobox.Trigger />
|
|
68
|
+
</Combobox.Input>
|
|
69
|
+
<Combobox.Popup>
|
|
70
|
+
<Combobox.Item value="admin"><Combobox.ItemText>Admin</Combobox.ItemText></Combobox.Item>
|
|
71
|
+
<Combobox.Item value="editor"><Combobox.ItemText>Editor</Combobox.ItemText></Combobox.Item>
|
|
72
|
+
<Combobox.Item value="viewer"><Combobox.ItemText>Viewer</Combobox.ItemText></Combobox.Item>
|
|
73
|
+
<Combobox.Item value="moderator"><Combobox.ItemText>Moderator</Combobox.ItemText></Combobox.Item>
|
|
74
|
+
<Combobox.Item value="analyst"><Combobox.ItemText>Analyst</Combobox.ItemText></Combobox.Item>
|
|
75
|
+
<Combobox.Empty>No roles found</Combobox.Empty>
|
|
76
|
+
</Combobox.Popup>
|
|
77
|
+
</Combobox.Root>
|
|
78
|
+
<span style={{fontSize: 14, color: '#888'}}>
|
|
79
|
+
Selected: {value ?? 'none'}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Controlled: Story = {
|
|
86
|
+
render: () => <ControlledDemo />,
|
|
87
|
+
play: async ({canvas}) => {
|
|
88
|
+
await expect(canvas.getByText('Selected: none')).toBeInTheDocument()
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const ManyItems: Story = {
|
|
93
|
+
render: () => {
|
|
94
|
+
const countries = [
|
|
95
|
+
'Argentina', 'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada',
|
|
96
|
+
'Chile', 'China', 'Colombia', 'Denmark', 'Egypt', 'Finland',
|
|
97
|
+
'France', 'Germany', 'Greece', 'India', 'Indonesia', 'Ireland',
|
|
98
|
+
'Italy', 'Japan', 'Kenya', 'Mexico', 'Netherlands', 'New Zealand',
|
|
99
|
+
'Norway', 'Peru', 'Poland', 'Portugal', 'South Korea', 'Spain',
|
|
100
|
+
'Sweden', 'Switzerland', 'Thailand', 'Turkey', 'United Kingdom', 'United States',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Combobox.Root>
|
|
105
|
+
<Combobox.Input placeholder="Search country...">
|
|
106
|
+
<Combobox.Trigger />
|
|
107
|
+
</Combobox.Input>
|
|
108
|
+
<Combobox.Popup>
|
|
109
|
+
{countries.map((country) => (
|
|
110
|
+
<Combobox.Item key={country} value={country.toLowerCase()}>
|
|
111
|
+
<Combobox.ItemText>{country}</Combobox.ItemText>
|
|
112
|
+
</Combobox.Item>
|
|
113
|
+
))}
|
|
114
|
+
<Combobox.Empty>No countries found</Combobox.Empty>
|
|
115
|
+
</Combobox.Popup>
|
|
116
|
+
</Combobox.Root>
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
}
|