@duro-app/ui 4.4.0 → 5.0.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/{Table-JTgcR_Fh.js → Table-BKN_d-mp.js} +506 -410
- package/dist/Table-BKN_d-mp.js.map +1 -0
- package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -1
- package/dist/components/ActionBar/styles.css.d.ts +7 -7
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
- package/dist/components/Alert/styles.css.d.ts +2 -2
- package/dist/components/Alert/styles.css.d.ts.map +1 -1
- package/dist/components/Badge/styles.css.d.ts +2 -2
- package/dist/components/Button/styles.css.d.ts +5 -5
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/Callout/styles.css.d.ts +1 -1
- package/dist/components/Callout/styles.css.d.ts.map +1 -1
- package/dist/components/Card/styles.css.d.ts +3 -3
- package/dist/components/Card/styles.css.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/styles.css.d.ts +7 -11
- package/dist/components/Checkbox/styles.css.d.ts.map +1 -1
- package/dist/components/Cluster/Cluster.stories.d.ts.map +1 -1
- package/dist/components/ColorInput/styles.css.d.ts +6 -6
- package/dist/components/ColorInput/styles.css.d.ts.map +1 -1
- package/dist/components/ColorMode/styles.css.d.ts +3 -3
- package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -1
- package/dist/components/Combobox/styles.css.d.ts +3 -3
- package/dist/components/Combobox/styles.css.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -1
- package/dist/components/DetailPanel/styles.css.d.ts +19 -10
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +18 -9
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.stories.d.ts.map +1 -1
- package/dist/components/DragDrop/styles.css.d.ts +0 -6
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerScroll.stories.d.ts +13 -0
- package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +19 -10
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/useSwipeDismiss.d.ts +7 -2
- package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
- package/dist/components/Field/Field.stories.d.ts.map +1 -1
- package/dist/components/Field/styles.css.d.ts +1 -1
- package/dist/components/Field/styles.css.d.ts.map +1 -1
- package/dist/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/components/Grid/Grid.d.ts +6 -4
- package/dist/components/Grid/Grid.d.ts.map +1 -1
- package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/components/Grid/styles.css.d.ts +3 -3
- package/dist/components/Input/Input.stories.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +3 -3
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.d.ts +4 -2
- package/dist/components/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.stories.d.ts.map +1 -1
- package/dist/components/InputGroup/styles.css.d.ts +3 -3
- package/dist/components/InputGroup/styles.css.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +1 -1
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/List/styles.css.d.ts +3 -3
- package/dist/components/List/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +4 -4
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
- package/dist/components/PageShell/styles.css.d.ts +3 -3
- package/dist/components/PageShell/styles.css.d.ts.map +1 -1
- package/dist/components/Panel/styles.css.d.ts +3 -3
- package/dist/components/Panel/styles.css.d.ts.map +1 -1
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
- package/dist/components/Popover/Popover.stories.d.ts.map +1 -1
- package/dist/components/Popover/PopoverLayerContext.d.ts +10 -0
- package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -0
- package/dist/components/Popover/PopoverLayers.stories.d.ts +13 -0
- package/dist/components/Popover/PopoverLayers.stories.d.ts.map +1 -0
- package/dist/components/Popover/styles.css.d.ts +2 -2
- package/dist/components/Popover/styles.css.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup/styles.css.d.ts +5 -9
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.d.ts +10 -3
- package/dist/components/ScrollArea/ScrollArea.d.ts.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +8 -8
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.meta.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +4 -4
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +7 -7
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts.map +1 -1
- package/dist/components/Spinner/styles.css.d.ts +10 -14
- package/dist/components/Spinner/styles.css.d.ts.map +1 -1
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Switch/styles.css.d.ts +10 -14
- package/dist/components/Switch/styles.css.d.ts.map +1 -1
- package/dist/components/Table/ColumnFilter.d.ts.map +1 -1
- package/dist/components/Table/FromTanstack.d.ts +2 -1
- package/dist/components/Table/FromTanstack.d.ts.map +1 -1
- package/dist/components/Table/Pagination.d.ts.map +1 -1
- package/dist/components/Table/SortIndicator.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +11 -9
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +6 -6
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/styles.css.d.ts +8 -8
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tag/Tag.stories.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +4 -4
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/TagGroup/TagGroup.d.ts.map +1 -1
- package/dist/components/TagGroup/styles.css.d.ts +4 -10
- package/dist/components/TagGroup/styles.css.d.ts.map +1 -1
- package/dist/components/Textarea/Textarea.stories.d.ts.map +1 -1
- package/dist/components/Textarea/styles.css.d.ts +3 -3
- package/dist/components/Textarea/styles.css.d.ts.map +1 -1
- package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
- package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
- package/dist/components/Toast/styles.css.d.ts +12 -12
- package/dist/components/Toast/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +14 -4
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +7 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +4 -0
- package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +6 -0
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts.map +1 -1
- package/dist/components/ToggleGroup/styles.css.d.ts +20 -1
- package/dist/components/ToggleGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ToggleGroup/useRovingFocus.d.ts +17 -0
- package/dist/components/ToggleGroup/useRovingFocus.d.ts.map +1 -0
- package/dist/components/Tooltip/styles.css.d.ts +1 -1
- package/dist/components/Tooltip/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +4 -4
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.stories.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +7 -7
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/docs/Spacing.stories.d.ts.map +1 -1
- package/dist/docs/Typography.stories.d.ts.map +1 -1
- package/dist/docs/helpers.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +3788 -3515
- package/dist/index.js.map +1 -1
- package/dist/shared/length.d.ts +24 -0
- package/dist/shared/length.d.ts.map +1 -0
- package/dist/shared/length.typecheck.d.ts +28 -0
- package/dist/shared/length.typecheck.d.ts.map +1 -0
- package/dist/styles/visually-hidden.css.d.ts +10 -0
- package/dist/styles/visually-hidden.css.d.ts.map +1 -0
- package/dist/table.js +166 -166
- package/dist/table.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +4 -2
- package/src/components/ActionBar/styles.css.ts +11 -9
- package/src/components/Alert/styles.css.ts +4 -3
- package/src/components/Badge/styles.css.ts +3 -3
- package/src/components/Button/styles.css.ts +8 -7
- package/src/components/Callout/styles.css.ts +2 -1
- package/src/components/Card/styles.css.ts +4 -3
- package/src/components/Checkbox/Checkbox.tsx +2 -1
- package/src/components/Checkbox/styles.css.ts +10 -11
- package/src/components/Cluster/Cluster.stories.tsx +2 -1
- package/src/components/ColorInput/styles.css.ts +9 -7
- package/src/components/ColorMode/styles.css.ts +4 -3
- package/src/components/Combobox/Combobox.meta.ts +6 -0
- package/src/components/Combobox/Combobox.tsx +5 -0
- package/src/components/Combobox/styles.css.ts +5 -3
- package/src/components/DetailPanel/DetailPanel.stories.tsx +5 -3
- package/src/components/DetailPanel/DetailPanel.tsx +3 -0
- package/src/components/DetailPanel/styles.css.ts +17 -11
- package/src/components/Dialog/Dialog.stories.tsx +2 -1
- package/src/components/Dialog/Dialog.tsx +3 -0
- package/src/components/Dialog/styles.css.ts +14 -9
- package/src/components/DragDrop/DragDrop.stories.tsx +2 -1
- package/src/components/DragDrop/DragDrop.tsx +2 -1
- package/src/components/DragDrop/styles.css.ts +0 -7
- package/src/components/Drawer/Drawer.meta.ts +1 -1
- package/src/components/Drawer/Drawer.tsx +15 -2
- package/src/components/Drawer/DrawerScroll.stories.tsx +292 -0
- package/src/components/Drawer/styles.css.ts +15 -10
- package/src/components/Drawer/useSwipeDismiss.ts +53 -8
- package/src/components/Field/Field.stories.tsx +3 -2
- package/src/components/Field/styles.css.ts +2 -1
- package/src/components/Form/Form.stories.tsx +4 -3
- package/src/components/Grid/Grid.meta.ts +2 -2
- package/src/components/Grid/Grid.stories.tsx +3 -2
- package/src/components/Grid/Grid.tsx +8 -6
- package/src/components/Grid/styles.css.ts +2 -2
- package/src/components/Input/Input.stories.tsx +2 -1
- package/src/components/Input/styles.css.ts +4 -3
- package/src/components/InputGroup/InputGroup.stories.tsx +3 -2
- package/src/components/InputGroup/InputGroup.tsx +5 -3
- package/src/components/InputGroup/styles.css.ts +4 -3
- package/src/components/LinkButton/styles.css.ts +2 -1
- package/src/components/List/List.stories.tsx +4 -2
- package/src/components/List/styles.css.ts +6 -4
- package/src/components/Menu/styles.css.ts +6 -4
- package/src/components/Menu/useMenuRoot.ts +5 -1
- package/src/components/PageShell/styles.css.ts +4 -3
- package/src/components/Panel/styles.css.ts +4 -3
- package/src/components/Popover/Popover.meta.ts +7 -0
- package/src/components/Popover/Popover.stories.tsx +6 -4
- package/src/components/Popover/Popover.tsx +30 -24
- package/src/components/Popover/PopoverLayerContext.ts +20 -0
- package/src/components/Popover/PopoverLayers.stories.tsx +234 -0
- package/src/components/Popover/styles.css.ts +3 -2
- package/src/components/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/RadioGroup/styles.css.ts +8 -9
- package/src/components/ScrollArea/ScrollArea.meta.ts +2 -2
- package/src/components/ScrollArea/ScrollArea.stories.tsx +7 -5
- package/src/components/ScrollArea/ScrollArea.tsx +18 -7
- package/src/components/ScrollArea/styles.css.ts +10 -9
- package/src/components/Select/Select.meta.ts +6 -0
- package/src/components/Select/Select.tsx +8 -1
- package/src/components/Select/styles.css.ts +6 -4
- package/src/components/Select/useSelectRoot.ts +6 -2
- package/src/components/SideNav/styles.css.ts +10 -8
- package/src/components/Spinner/Spinner.tsx +2 -1
- package/src/components/Spinner/styles.css.ts +14 -14
- package/src/components/Switch/Switch.tsx +2 -1
- package/src/components/Switch/styles.css.ts +15 -15
- package/src/components/Table/ColumnFilter.tsx +5 -4
- package/src/components/Table/FromTanstack.tsx +2 -1
- package/src/components/Table/Pagination.tsx +6 -4
- package/src/components/Table/SortIndicator.tsx +4 -2
- package/src/components/Table/Table.stories.tsx +8 -8
- package/src/components/Table/Table.tsx +19 -15
- package/src/components/Table/styles.css.ts +9 -7
- package/src/components/Tabs/styles.css.ts +11 -9
- package/src/components/Tag/Tag.stories.tsx +4 -2
- package/src/components/Tag/styles.css.ts +6 -5
- package/src/components/TagGroup/TagGroup.tsx +2 -1
- package/src/components/TagGroup/styles.css.ts +6 -10
- package/src/components/Textarea/Textarea.stories.tsx +2 -1
- package/src/components/Textarea/styles.css.ts +4 -3
- package/src/components/ThemeProvider/ThemeProvider.stories.tsx +7 -5
- package/src/components/Toast/Toast.stories.tsx +2 -1
- package/src/components/Toast/styles.css.ts +16 -14
- package/src/components/Toggle/Toggle.meta.ts +1 -1
- package/src/components/Toggle/Toggle.tsx +19 -2
- package/src/components/Toggle/styles.css.ts +18 -4
- package/src/components/ToggleGroup/ToggleGroup.meta.ts +11 -1
- package/src/components/ToggleGroup/ToggleGroup.stories.tsx +157 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +87 -10
- package/src/components/ToggleGroup/ToggleGroupContext.ts +6 -0
- package/src/components/ToggleGroup/styles.css.ts +32 -2
- package/src/components/ToggleGroup/useRovingFocus.ts +121 -0
- package/src/components/Tooltip/styles.css.ts +2 -1
- package/src/components/Tree/styles.css.ts +8 -6
- package/src/components/VirtualTable/VirtualTable.stories.tsx +4 -3
- package/src/components/VirtualTable/VirtualTable.tsx +5 -3
- package/src/components/VirtualTable/styles.css.ts +9 -8
- package/src/docs/Spacing.stories.tsx +11 -10
- package/src/docs/Typography.stories.tsx +8 -6
- package/src/docs/helpers.tsx +3 -2
- package/src/shared/length.ts +51 -0
- package/src/shared/length.typecheck.tsx +47 -0
- package/src/styles/visually-hidden.css.ts +16 -0
- package/dist/Table-JTgcR_Fh.js.map +0 -1
|
@@ -3,6 +3,7 @@ import {expect, fn} from 'storybook/test'
|
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {Textarea} from './Textarea'
|
|
5
5
|
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
6
7
|
|
|
7
8
|
const meta: Meta<typeof Textarea> = {
|
|
8
9
|
title: 'Components/Textarea',
|
|
@@ -76,7 +77,7 @@ export const CustomRows: Story = {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const stackStyles = css.create({
|
|
79
|
-
stack: {display: 'flex', flexDirection: 'column', gap: spacing.ms, maxWidth:
|
|
80
|
+
stack: {display: 'flex', flexDirection: 'column', gap: spacing.ms, maxWidth: sizes.panelSm},
|
|
80
81
|
})
|
|
81
82
|
|
|
82
83
|
export const AllVariants: Story = {
|
|
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
7
|
|
|
7
8
|
export const styles = css.create({
|
|
8
9
|
base: {
|
|
@@ -16,7 +17,7 @@ export const styles = css.create({
|
|
|
16
17
|
lineHeight: typography.lineHeight,
|
|
17
18
|
color: colors.text,
|
|
18
19
|
backgroundColor: colors.bg,
|
|
19
|
-
borderWidth:
|
|
20
|
+
borderWidth: borders.hairline,
|
|
20
21
|
borderStyle: 'solid',
|
|
21
22
|
borderRadius: radii.sm,
|
|
22
23
|
resize: 'vertical' as const,
|
|
@@ -25,7 +26,7 @@ export const styles = css.create({
|
|
|
25
26
|
transitionTimingFunction: easing.standard,
|
|
26
27
|
outlineWidth: {
|
|
27
28
|
default: 0,
|
|
28
|
-
':focus-visible':
|
|
29
|
+
':focus-visible': borders.focusRing,
|
|
29
30
|
},
|
|
30
31
|
outlineStyle: {
|
|
31
32
|
default: 'none',
|
|
@@ -37,7 +38,7 @@ export const styles = css.create({
|
|
|
37
38
|
},
|
|
38
39
|
outlineOffset: {
|
|
39
40
|
default: 0,
|
|
40
|
-
':focus-visible':
|
|
41
|
+
':focus-visible': borders.focusOffsetSm,
|
|
41
42
|
},
|
|
42
43
|
},
|
|
43
44
|
default: {
|
|
@@ -7,6 +7,8 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
7
7
|
import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
8
8
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
9
9
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
10
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
11
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
10
12
|
|
|
11
13
|
const meta: Meta<typeof ThemeProvider> = {
|
|
12
14
|
title: 'Theme/ThemeProvider',
|
|
@@ -27,7 +29,7 @@ const sampleStyles = css.create({
|
|
|
27
29
|
card: {
|
|
28
30
|
padding: spacing.md,
|
|
29
31
|
backgroundColor: colors.bgCard,
|
|
30
|
-
borderWidth:
|
|
32
|
+
borderWidth: borders.hairline,
|
|
31
33
|
borderStyle: 'solid',
|
|
32
34
|
borderColor: colors.border,
|
|
33
35
|
borderRadius: radii.md,
|
|
@@ -53,10 +55,10 @@ const sampleStyles = css.create({
|
|
|
53
55
|
flexWrap: 'wrap',
|
|
54
56
|
},
|
|
55
57
|
swatch: {
|
|
56
|
-
width:
|
|
57
|
-
height:
|
|
58
|
+
width: sizes.iconXxl,
|
|
59
|
+
height: sizes.iconXxl,
|
|
58
60
|
borderRadius: radii.sm,
|
|
59
|
-
borderWidth:
|
|
61
|
+
borderWidth: borders.hairline,
|
|
60
62
|
borderStyle: 'solid',
|
|
61
63
|
borderColor: colors.border,
|
|
62
64
|
},
|
|
@@ -131,7 +133,7 @@ const sideBySideStyles = css.create({
|
|
|
131
133
|
},
|
|
132
134
|
column: {
|
|
133
135
|
flex: 1,
|
|
134
|
-
minWidth:
|
|
136
|
+
minWidth: sizes.gridColMd,
|
|
135
137
|
},
|
|
136
138
|
label: {
|
|
137
139
|
fontSize: typography.fontSizeSm,
|
|
@@ -3,13 +3,14 @@ import {expect, userEvent, waitFor} from 'storybook/test'
|
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {ToastProvider, useToast} from './ToastProvider'
|
|
5
5
|
import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
7
|
|
|
7
8
|
const s = css.create({
|
|
8
9
|
row: {display: 'flex', flexDirection: 'row', gap: spacing.sm, flexWrap: 'wrap'},
|
|
9
10
|
btn: {
|
|
10
11
|
padding: spacing.sm,
|
|
11
12
|
borderRadius: radii.sm,
|
|
12
|
-
borderWidth:
|
|
13
|
+
borderWidth: borders.hairline,
|
|
13
14
|
borderStyle: 'solid',
|
|
14
15
|
cursor: 'pointer',
|
|
15
16
|
},
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {css} from 'react-strict-dom'
|
|
2
2
|
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
-
import {
|
|
3
|
+
import {microSpacing, radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
-
import {easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
7
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
7
9
|
|
|
8
10
|
const enter = css.keyframes({
|
|
9
11
|
from: {opacity: 0, transform: 'translateY(10px)'},
|
|
@@ -31,17 +33,17 @@ export const styles = css.create({
|
|
|
31
33
|
pointerEvents: 'auto',
|
|
32
34
|
boxSizing: 'border-box',
|
|
33
35
|
width: '100%',
|
|
34
|
-
maxWidth:
|
|
36
|
+
maxWidth: sizes.toastMaxW,
|
|
35
37
|
display: 'flex',
|
|
36
38
|
flexDirection: 'row',
|
|
37
39
|
alignItems: 'flex-start',
|
|
38
40
|
gap: spacing.sm,
|
|
39
41
|
padding: spacing.md,
|
|
40
42
|
borderRadius: radii.sm,
|
|
41
|
-
borderWidth:
|
|
43
|
+
borderWidth: borders.hairline,
|
|
42
44
|
borderStyle: 'solid',
|
|
43
45
|
borderColor: colors.border,
|
|
44
|
-
borderLeftWidth:
|
|
46
|
+
borderLeftWidth: borders.accent,
|
|
45
47
|
backgroundColor: colors.bgCard,
|
|
46
48
|
boxShadow: shadows.lg,
|
|
47
49
|
color: colors.text,
|
|
@@ -51,7 +53,7 @@ export const styles = css.create({
|
|
|
51
53
|
default: enter,
|
|
52
54
|
'@media (prefers-reduced-motion: reduce)': 'none',
|
|
53
55
|
},
|
|
54
|
-
animationDuration:
|
|
56
|
+
animationDuration: duration.brisk,
|
|
55
57
|
animationTimingFunction: easing.easeOut,
|
|
56
58
|
},
|
|
57
59
|
success: {borderLeftColor: colors.successBorder},
|
|
@@ -63,7 +65,7 @@ export const styles = css.create({
|
|
|
63
65
|
display: 'flex',
|
|
64
66
|
alignItems: 'center',
|
|
65
67
|
justifyContent: 'center',
|
|
66
|
-
paddingTop:
|
|
68
|
+
paddingTop: microSpacing.px1,
|
|
67
69
|
},
|
|
68
70
|
iconSuccess: {color: colors.successText},
|
|
69
71
|
iconError: {color: colors.errorText},
|
|
@@ -74,14 +76,14 @@ export const styles = css.create({
|
|
|
74
76
|
minWidth: 0,
|
|
75
77
|
display: 'flex',
|
|
76
78
|
flexDirection: 'column',
|
|
77
|
-
gap:
|
|
79
|
+
gap: microSpacing.px2,
|
|
78
80
|
},
|
|
79
81
|
message: {
|
|
80
82
|
color: colors.text,
|
|
81
83
|
},
|
|
82
84
|
action: {
|
|
83
85
|
alignSelf: 'flex-start',
|
|
84
|
-
marginTop:
|
|
86
|
+
marginTop: microSpacing.px2,
|
|
85
87
|
padding: 0,
|
|
86
88
|
borderWidth: 0,
|
|
87
89
|
backgroundColor: 'transparent',
|
|
@@ -91,9 +93,9 @@ export const styles = css.create({
|
|
|
91
93
|
textDecorationLine: 'underline',
|
|
92
94
|
cursor: 'pointer',
|
|
93
95
|
outlineStyle: {default: 'none', ':focus-visible': 'solid'},
|
|
94
|
-
outlineWidth:
|
|
96
|
+
outlineWidth: borders.focusRing,
|
|
95
97
|
outlineColor: colors.accent,
|
|
96
|
-
outlineOffset:
|
|
98
|
+
outlineOffset: borders.focusOffset,
|
|
97
99
|
borderRadius: radii.sm,
|
|
98
100
|
},
|
|
99
101
|
closeBtn: {
|
|
@@ -101,7 +103,7 @@ export const styles = css.create({
|
|
|
101
103
|
display: 'flex',
|
|
102
104
|
alignItems: 'center',
|
|
103
105
|
justifyContent: 'center',
|
|
104
|
-
padding:
|
|
106
|
+
padding: microSpacing.px2,
|
|
105
107
|
marginTop: -1,
|
|
106
108
|
borderWidth: 0,
|
|
107
109
|
backgroundColor: 'transparent',
|
|
@@ -109,8 +111,8 @@ export const styles = css.create({
|
|
|
109
111
|
cursor: 'pointer',
|
|
110
112
|
borderRadius: radii.sm,
|
|
111
113
|
outlineStyle: {default: 'none', ':focus-visible': 'solid'},
|
|
112
|
-
outlineWidth:
|
|
114
|
+
outlineWidth: borders.focusRing,
|
|
113
115
|
outlineColor: colors.accent,
|
|
114
|
-
outlineOffset:
|
|
116
|
+
outlineOffset: borders.focusOffsetSm,
|
|
115
117
|
},
|
|
116
118
|
})
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Toggle button with pressed/unpressed state. Works standalone or within ToggleGroup for multi/single selection.',
|
|
5
|
+
'Toggle button with pressed/unpressed state. Works standalone or within ToggleGroup for multi/single selection. Under a coarse pointer (touch) every Toggle is at least sizes.touchTarget (44px) square; in a wrapping ToggleGroup each keeps its own border and a fixed row height.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Toolbar buttons that toggle on/off (bold, italic, grid view)',
|
|
8
8
|
'Within ToggleGroup for selecting from visible options',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type ReactNode, useCallback} from 'react'
|
|
1
|
+
import {type ReactNode, useCallback, useLayoutEffect, useRef} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
4
|
import {useToggleGroup} from '../ToggleGroup/ToggleGroupContext'
|
|
@@ -23,6 +23,11 @@ interface ToggleProps {
|
|
|
23
23
|
children: ReactNode
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const wrappedSizeMap = {
|
|
27
|
+
default: styles.wrappedDefault,
|
|
28
|
+
small: styles.wrappedSmall,
|
|
29
|
+
} as const
|
|
30
|
+
|
|
26
31
|
const sizeMap = {
|
|
27
32
|
default: styles.sizeDefault,
|
|
28
33
|
small: styles.sizeSmall,
|
|
@@ -46,6 +51,14 @@ export function Toggle({
|
|
|
46
51
|
const disabled = disabledProp || (group?.disabled ?? false)
|
|
47
52
|
const size = group?.size ?? sizeProp
|
|
48
53
|
|
|
54
|
+
const wrap = group?.wrap ?? false
|
|
55
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
56
|
+
const register = group?.register
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
if (!wrap || !register || value === undefined || !ref.current) return
|
|
59
|
+
return register(value, ref.current)
|
|
60
|
+
}, [wrap, register, value])
|
|
61
|
+
|
|
49
62
|
const [standalonePressed, setStandalonePressed] = useControllableValue(
|
|
50
63
|
controlledPressed,
|
|
51
64
|
defaultPressed,
|
|
@@ -65,7 +78,10 @@ export function Toggle({
|
|
|
65
78
|
|
|
66
79
|
return (
|
|
67
80
|
<html.button
|
|
81
|
+
ref={ref}
|
|
68
82
|
type="button"
|
|
83
|
+
tabIndex={wrap && value !== undefined ? (group?.tabStopValue === value ? 0 : -1) : undefined}
|
|
84
|
+
onFocus={wrap && value !== undefined ? () => group?.onItemFocus(value) : undefined}
|
|
69
85
|
aria-pressed={pressed}
|
|
70
86
|
aria-label={ariaLabel}
|
|
71
87
|
disabled={disabled}
|
|
@@ -75,7 +91,8 @@ export function Toggle({
|
|
|
75
91
|
styles.base,
|
|
76
92
|
sizeMap[size],
|
|
77
93
|
pressed ? styles.pressed : styles.unpressed,
|
|
78
|
-
|
|
94
|
+
wrap && wrappedSizeMap[size],
|
|
95
|
+
isGrouped && !wrap && styles.grouped,
|
|
79
96
|
disabled && styles.disabled,
|
|
80
97
|
]}
|
|
81
98
|
>
|
|
@@ -3,13 +3,15 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
7
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
8
|
|
|
7
9
|
export const styles = css.create({
|
|
8
10
|
base: {
|
|
9
11
|
display: 'inline-flex',
|
|
10
12
|
alignItems: 'center',
|
|
11
13
|
justifyContent: 'center',
|
|
12
|
-
borderWidth:
|
|
14
|
+
borderWidth: borders.hairline,
|
|
13
15
|
borderStyle: 'solid',
|
|
14
16
|
cursor: 'pointer',
|
|
15
17
|
fontFamily: typography.fontFamily,
|
|
@@ -17,10 +19,12 @@ export const styles = css.create({
|
|
|
17
19
|
transitionProperty: 'background-color, border-color, color, opacity',
|
|
18
20
|
transitionDuration: duration.fast,
|
|
19
21
|
transitionTimingFunction: easing.standard,
|
|
20
|
-
outlineWidth: {default: 0, ':focus-visible':
|
|
22
|
+
outlineWidth: {default: 0, ':focus-visible': borders.focusRing},
|
|
21
23
|
outlineStyle: 'solid',
|
|
22
24
|
outlineColor: colors.accent,
|
|
23
|
-
outlineOffset:
|
|
25
|
+
outlineOffset: borders.focusOffset,
|
|
26
|
+
minWidth: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
|
|
27
|
+
minHeight: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
|
|
24
28
|
},
|
|
25
29
|
sizeDefault: {
|
|
26
30
|
padding: `${spacing.sm} ${spacing.md}`,
|
|
@@ -34,6 +38,16 @@ export const styles = css.create({
|
|
|
34
38
|
borderRadius: radii.sm,
|
|
35
39
|
gap: spacing.xs,
|
|
36
40
|
},
|
|
41
|
+
// wrapped toggles: a fixed block size per size, the touch target under a
|
|
42
|
+
// coarse pointer. ToggleGroup's maxRows arithmetic reads the same tokens.
|
|
43
|
+
wrappedDefault: {
|
|
44
|
+
boxSizing: 'border-box',
|
|
45
|
+
height: {default: sizes.controlMd, '@media (pointer: coarse)': sizes.touchTarget},
|
|
46
|
+
},
|
|
47
|
+
wrappedSmall: {
|
|
48
|
+
boxSizing: 'border-box',
|
|
49
|
+
height: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
50
|
+
},
|
|
37
51
|
unpressed: {
|
|
38
52
|
backgroundColor: {
|
|
39
53
|
default: 'transparent',
|
|
@@ -56,7 +70,7 @@ export const styles = css.create({
|
|
|
56
70
|
grouped: {
|
|
57
71
|
borderWidth: 0,
|
|
58
72
|
borderRadius: 0,
|
|
59
|
-
borderRightWidth:
|
|
73
|
+
borderRightWidth: borders.hairline,
|
|
60
74
|
borderRightStyle: 'solid',
|
|
61
75
|
// Override any borderColor set by pressed state — dividers always use border token
|
|
62
76
|
borderColor: colors.border,
|
|
@@ -6,10 +6,11 @@ export const meta: ComponentMeta = {
|
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Visible option set where user picks one or more (view mode, filter categories)',
|
|
8
8
|
'Segmented control pattern',
|
|
9
|
+
'A visible grid of icon choices: wrap + maxRows (rows scroll past maxRows, one tab stop with arrow keys, sizes.touchTarget (44px) touch targets)',
|
|
9
10
|
],
|
|
10
11
|
whenNotToUse: [
|
|
11
12
|
'Dropdown selection — use Select',
|
|
12
|
-
'Many options (5+) — use Select for space efficiency',
|
|
13
|
+
'Many text options (5+) — use Select for space efficiency; does not apply to icon-only choices, which keep options visible and open no layer',
|
|
13
14
|
'Form checkboxes — use Checkbox group in Fieldset',
|
|
14
15
|
],
|
|
15
16
|
relatedTo: [
|
|
@@ -30,5 +31,14 @@ export const meta: ComponentMeta = {
|
|
|
30
31
|
<Toggle value="list">List view</Toggle>
|
|
31
32
|
<Toggle value="grid">Grid view</Toggle>
|
|
32
33
|
<Toggle value="board">Board view</Toggle>
|
|
34
|
+
</ToggleGroup>
|
|
35
|
+
|
|
36
|
+
// Icon choices: wraps onto rows, scrolls past three, one tab stop
|
|
37
|
+
<ToggleGroup wrap maxRows={3} size="small" value={[icon]} onValueChange={(v) => v[0] && setIcon(v[0])} aria-label="Icon">
|
|
38
|
+
{names.map((name) => (
|
|
39
|
+
<Toggle key={name} value={name} aria-label={name}>
|
|
40
|
+
<Icon name={name} size="sm" />
|
|
41
|
+
</Toggle>
|
|
42
|
+
))}
|
|
33
43
|
</ToggleGroup>`,
|
|
34
44
|
}
|
|
@@ -3,7 +3,10 @@ import {expect, fn} from 'storybook/test'
|
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {ToggleGroup} from './ToggleGroup'
|
|
5
5
|
import {Toggle} from '../Toggle/Toggle'
|
|
6
|
+
import {Icon, type IconName} from '../Icon'
|
|
7
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
6
8
|
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
9
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
7
10
|
|
|
8
11
|
const meta: Meta<typeof ToggleGroup> = {
|
|
9
12
|
title: 'Components/ToggleGroup',
|
|
@@ -158,3 +161,157 @@ export const AllVariants: Story = {
|
|
|
158
161
|
await expect(groups.length).toBe(4)
|
|
159
162
|
},
|
|
160
163
|
}
|
|
164
|
+
|
|
165
|
+
const ICON_NAMES: IconName[] = [
|
|
166
|
+
'x-circle',
|
|
167
|
+
'check-circle',
|
|
168
|
+
'check-done',
|
|
169
|
+
'clock',
|
|
170
|
+
'forbidden',
|
|
171
|
+
'info-circle',
|
|
172
|
+
'alert-triangle',
|
|
173
|
+
'shield',
|
|
174
|
+
'lock',
|
|
175
|
+
'key',
|
|
176
|
+
'map',
|
|
177
|
+
'layers',
|
|
178
|
+
'repeat',
|
|
179
|
+
'database',
|
|
180
|
+
'shield-check',
|
|
181
|
+
'route',
|
|
182
|
+
'git-branch',
|
|
183
|
+
'menu',
|
|
184
|
+
'pin',
|
|
185
|
+
'server',
|
|
186
|
+
'hard-drive',
|
|
187
|
+
'box',
|
|
188
|
+
'image',
|
|
189
|
+
'tag',
|
|
190
|
+
'pie-chart',
|
|
191
|
+
'users',
|
|
192
|
+
'user-plus',
|
|
193
|
+
'mail',
|
|
194
|
+
'file-text',
|
|
195
|
+
'plug',
|
|
196
|
+
'search',
|
|
197
|
+
'mic',
|
|
198
|
+
'sun',
|
|
199
|
+
'moon',
|
|
200
|
+
'monitor',
|
|
201
|
+
'contrast',
|
|
202
|
+
'info-circle-filled',
|
|
203
|
+
'alert-triangle-filled',
|
|
204
|
+
'check-circle-filled',
|
|
205
|
+
'x-circle-filled',
|
|
206
|
+
'shield-filled',
|
|
207
|
+
'lock-filled',
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
const iconStyles = css.create({
|
|
211
|
+
frame: {width: sizes.panelSm},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
function IconChoices({
|
|
215
|
+
names,
|
|
216
|
+
pressed,
|
|
217
|
+
size = 'small',
|
|
218
|
+
}: {
|
|
219
|
+
names: IconName[]
|
|
220
|
+
pressed: string
|
|
221
|
+
size?: 'small' | 'default'
|
|
222
|
+
}) {
|
|
223
|
+
return (
|
|
224
|
+
<html.div style={iconStyles.frame}>
|
|
225
|
+
<ToggleGroup wrap maxRows={3} size={size} defaultValue={[pressed]} aria-label="Icon">
|
|
226
|
+
{names.map((name) => (
|
|
227
|
+
<Toggle key={name} value={name} aria-label={name}>
|
|
228
|
+
<Icon name={name} size="sm" />
|
|
229
|
+
</Toggle>
|
|
230
|
+
))}
|
|
231
|
+
</ToggleGroup>
|
|
232
|
+
</html.div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const IconChoicesStory: Story = {
|
|
237
|
+
name: 'Icon choices',
|
|
238
|
+
render: () => <IconChoices names={ICON_NAMES} pressed={ICON_NAMES[39]} />,
|
|
239
|
+
play: async ({canvas, canvasElement}) => {
|
|
240
|
+
const scrollTopBefore = document.scrollingElement?.scrollTop
|
|
241
|
+
await expect(ICON_NAMES.length).toBe(42)
|
|
242
|
+
const viewport = canvasElement.querySelector<HTMLElement>('[data-duro-scroll]')!
|
|
243
|
+
await expect(Math.abs(viewport.clientHeight - 118)).toBeLessThanOrEqual(2)
|
|
244
|
+
|
|
245
|
+
const pressed = canvas.getByRole('button', {name: ICON_NAMES[39]})
|
|
246
|
+
const other = canvas.getByRole('button', {name: ICON_NAMES[0]})
|
|
247
|
+
await expect(pressed).toHaveAttribute('aria-pressed', 'true')
|
|
248
|
+
for (const el of [pressed, other]) {
|
|
249
|
+
const cs = getComputedStyle(el)
|
|
250
|
+
await expect(cs.borderTopWidth).toBe('1px')
|
|
251
|
+
await expect(cs.borderRightWidth).toBe('1px')
|
|
252
|
+
await expect(cs.borderBottomWidth).toBe('1px')
|
|
253
|
+
await expect(cs.borderLeftWidth).toBe('1px')
|
|
254
|
+
// radii.sm
|
|
255
|
+
await expect(cs.borderTopLeftRadius).toBe('8px')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// the pressed toggle is visible inside the viewport
|
|
259
|
+
const v = viewport.getBoundingClientRect()
|
|
260
|
+
const p = pressed.getBoundingClientRect()
|
|
261
|
+
await expect(p.top).toBeGreaterThanOrEqual(v.top - 1)
|
|
262
|
+
await expect(p.bottom).toBeLessThanOrEqual(v.bottom + 1)
|
|
263
|
+
await expect(document.scrollingElement?.scrollTop).toBe(scrollTopBefore)
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const FiveIcons: Story = {
|
|
268
|
+
name: 'Five icons',
|
|
269
|
+
render: () => <IconChoices names={ICON_NAMES.slice(0, 5)} pressed={ICON_NAMES[0]} />,
|
|
270
|
+
play: async ({canvas}) => {
|
|
271
|
+
const group = canvas.getByRole('toolbar')
|
|
272
|
+
await expect(group.getBoundingClientRect().height).toBe(SIZES_PX.controlSm)
|
|
273
|
+
// exact: Toggle's styles read the same size tokens, and a drift must fail here
|
|
274
|
+
const toggle = canvas.getAllByRole('button')[0].getBoundingClientRect()
|
|
275
|
+
await expect(Math.abs(toggle.height - SIZES_PX.controlSm)).toBeLessThanOrEqual(0.5)
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const KeyboardRoving: Story = {
|
|
280
|
+
render: () => <IconChoices names={ICON_NAMES} pressed={ICON_NAMES[3]} />,
|
|
281
|
+
play: async ({canvas, userEvent}) => {
|
|
282
|
+
const btn = (i: number) => canvas.getByRole('button', {name: ICON_NAMES[i]})
|
|
283
|
+
await userEvent.tab()
|
|
284
|
+
await expect(btn(3)).toHaveFocus()
|
|
285
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
286
|
+
await expect(btn(4)).toHaveFocus()
|
|
287
|
+
await userEvent.keyboard('{ArrowLeft}{ArrowLeft}')
|
|
288
|
+
await expect(btn(2)).toHaveFocus()
|
|
289
|
+
// Down moves to the next row, Up back
|
|
290
|
+
const top = btn(2).getBoundingClientRect().top
|
|
291
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
292
|
+
const active = document.activeElement as HTMLElement
|
|
293
|
+
await expect(active.getBoundingClientRect().top).toBeGreaterThan(top + 20)
|
|
294
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
295
|
+
await expect(btn(2)).toHaveFocus()
|
|
296
|
+
await userEvent.keyboard('{End}')
|
|
297
|
+
await expect(btn(41)).toHaveFocus()
|
|
298
|
+
await userEvent.keyboard('{Home}')
|
|
299
|
+
await expect(btn(0)).toHaveFocus()
|
|
300
|
+
await userEvent.keyboard('{Enter}')
|
|
301
|
+
await expect(btn(0)).toHaveAttribute('aria-pressed', 'true')
|
|
302
|
+
// a second tab leaves the group
|
|
303
|
+
await userEvent.tab()
|
|
304
|
+
await expect(canvas.getAllByRole('button')).not.toContain(document.activeElement)
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const DefaultSizeRows: Story = {
|
|
309
|
+
name: 'Default size rows',
|
|
310
|
+
render: () => (
|
|
311
|
+
<IconChoices names={ICON_NAMES.slice(0, 5)} pressed={ICON_NAMES[0]} size="default" />
|
|
312
|
+
),
|
|
313
|
+
play: async ({canvas}) => {
|
|
314
|
+
const toggle = canvas.getAllByRole('button')[0].getBoundingClientRect()
|
|
315
|
+
await expect(Math.abs(toggle.height - SIZES_PX.controlMd)).toBeLessThanOrEqual(0.5)
|
|
316
|
+
},
|
|
317
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {type ReactNode, useCallback} from 'react'
|
|
1
|
+
import {type ReactNode, useCallback, useLayoutEffect, useRef} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
4
|
import type {ToggleSize} from '../Toggle/Toggle'
|
|
5
5
|
import {ToggleGroupContext, type Orientation} from './ToggleGroupContext'
|
|
6
6
|
import {useFieldGroupLabelling} from '../Field/FieldContext'
|
|
7
|
+
import {ScrollArea} from '../ScrollArea/ScrollArea'
|
|
8
|
+
import {useRovingFocus} from './useRovingFocus'
|
|
7
9
|
import {styles} from './styles.css'
|
|
8
10
|
|
|
9
11
|
interface ToggleGroupProps {
|
|
@@ -21,6 +23,16 @@ interface ToggleGroupProps {
|
|
|
21
23
|
orientation?: Orientation
|
|
22
24
|
/** Size applied to all child toggles. */
|
|
23
25
|
size?: ToggleSize
|
|
26
|
+
/** Items wrap onto rows, each with its own border (default false). Enables
|
|
27
|
+
* roving focus: one tab stop, arrow keys move between items. */
|
|
28
|
+
wrap?: boolean
|
|
29
|
+
// holds-until: the registry generator (packages/cli/scripts) reads a
|
|
30
|
+
// discriminated union. `{wrap?: false; maxRows?: never} | {wrap: true;
|
|
31
|
+
// maxRows?: number}` is the right type, but today it drops both props from
|
|
32
|
+
// the registry the CLI and MCP serve.
|
|
33
|
+
/** Caps a wrapping group at this many rows (plus half a row, hinting more)
|
|
34
|
+
* and scrolls the rest. Only with `wrap`; ignored without it. */
|
|
35
|
+
maxRows?: number
|
|
24
36
|
/** Accessible name when the group is not inside a Field.Root (inside one,
|
|
25
37
|
* the Field.Label names it). */
|
|
26
38
|
'aria-label'?: string
|
|
@@ -37,6 +49,8 @@ export function ToggleGroup({
|
|
|
37
49
|
disabled = false,
|
|
38
50
|
orientation = 'horizontal',
|
|
39
51
|
size = 'default',
|
|
52
|
+
wrap = false,
|
|
53
|
+
maxRows,
|
|
40
54
|
children,
|
|
41
55
|
...labelling
|
|
42
56
|
}: ToggleGroupProps) {
|
|
@@ -57,16 +71,79 @@ export function ToggleGroup({
|
|
|
57
71
|
[value, multiple, setValue],
|
|
58
72
|
)
|
|
59
73
|
|
|
74
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
75
|
+
const roving = useRovingFocus({enabled: wrap, pressed: value, rootRef})
|
|
76
|
+
const scrolls = wrap && maxRows !== undefined
|
|
77
|
+
const pressedValue = value[0]
|
|
78
|
+
|
|
79
|
+
// bring the pressed toggle into view by moving the viewport itself, never
|
|
80
|
+
// scrollIntoView, which would scroll the page too
|
|
81
|
+
const {items} = roving
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
if (!scrolls || pressedValue === undefined) return
|
|
84
|
+
const viewport = rootRef.current?.closest<HTMLElement>('[data-duro-scroll]')
|
|
85
|
+
if (!viewport) return
|
|
86
|
+
// Measured against the viewport's own box, not offsetTop: a positioned
|
|
87
|
+
// ancestor (a Drawer panel) would otherwise be the reference.
|
|
88
|
+
const centre = () => {
|
|
89
|
+
const el = items.current.get(pressedValue)
|
|
90
|
+
if (!el || viewport.clientHeight === 0) return false
|
|
91
|
+
const top =
|
|
92
|
+
el.getBoundingClientRect().top - viewport.getBoundingClientRect().top + viewport.scrollTop
|
|
93
|
+
const centered = top - (viewport.clientHeight - el.offsetHeight) / 2
|
|
94
|
+
const max = viewport.scrollHeight - viewport.clientHeight
|
|
95
|
+
viewport.scrollTop = Math.min(Math.max(centered, 0), Math.max(max, 0))
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
if (centre()) return
|
|
99
|
+
// Not laid out yet (a container still opening): centre once it has a size.
|
|
100
|
+
const observer = new ResizeObserver(() => {
|
|
101
|
+
if (centre()) observer.disconnect()
|
|
102
|
+
})
|
|
103
|
+
observer.observe(viewport)
|
|
104
|
+
return () => observer.disconnect()
|
|
105
|
+
}, [scrolls, pressedValue, items])
|
|
106
|
+
|
|
107
|
+
const group = (
|
|
108
|
+
<html.div
|
|
109
|
+
ref={rootRef}
|
|
110
|
+
role="toolbar"
|
|
111
|
+
aria-orientation={orientation}
|
|
112
|
+
{...a11y}
|
|
113
|
+
style={[styles.root, orientation === 'vertical' && styles.vertical, wrap && styles.wrap]}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</html.div>
|
|
117
|
+
)
|
|
118
|
+
|
|
60
119
|
return (
|
|
61
|
-
<ToggleGroupContext.Provider
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
<ToggleGroupContext.Provider
|
|
121
|
+
value={{
|
|
122
|
+
value,
|
|
123
|
+
toggle,
|
|
124
|
+
disabled,
|
|
125
|
+
orientation,
|
|
126
|
+
size,
|
|
127
|
+
wrap,
|
|
128
|
+
tabStopValue: roving.tabStopValue,
|
|
129
|
+
register: roving.register,
|
|
130
|
+
onItemFocus: roving.onItemFocus,
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{scrolls ? (
|
|
134
|
+
<ScrollArea.Root>
|
|
135
|
+
<ScrollArea.Viewport
|
|
136
|
+
rovingOwner
|
|
137
|
+
style={size === 'small' ? styles.maxRowsSmall(maxRows) : styles.maxRowsDefault(maxRows)}
|
|
138
|
+
>
|
|
139
|
+
<ScrollArea.Content>
|
|
140
|
+
<html.div style={styles.scrollContent}>{group}</html.div>
|
|
141
|
+
</ScrollArea.Content>
|
|
142
|
+
</ScrollArea.Viewport>
|
|
143
|
+
</ScrollArea.Root>
|
|
144
|
+
) : (
|
|
145
|
+
group
|
|
146
|
+
)}
|
|
70
147
|
</ToggleGroupContext.Provider>
|
|
71
148
|
)
|
|
72
149
|
}
|
|
@@ -10,6 +10,12 @@ export interface ToggleGroupContextValue {
|
|
|
10
10
|
disabled: boolean
|
|
11
11
|
orientation: Orientation
|
|
12
12
|
size: ToggleSize
|
|
13
|
+
/** Items wrap onto rows with their own borders (and roving focus). */
|
|
14
|
+
wrap: boolean
|
|
15
|
+
/** The item holding the group's single tab stop (wrap only). */
|
|
16
|
+
tabStopValue: string | null
|
|
17
|
+
register: (itemValue: string, el: HTMLElement) => () => void
|
|
18
|
+
onItemFocus: (itemValue: string) => void
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export const ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null)
|