@duro-app/ui 5.12.0 → 5.14.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/README.md +49 -0
- package/dist/Table-DnaoH-h5.js +1806 -0
- package/dist/Table-DnaoH-h5.js.map +1 -0
- package/dist/components/AppShell/native.css.d.ts +53 -0
- package/dist/components/AppShell/native.css.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/ColorMode/ColorMode.d.ts +2 -1
- package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
- package/dist/components/ColorMode/storage.d.ts +4 -0
- package/dist/components/ColorMode/storage.d.ts.map +1 -0
- package/dist/components/ColorMode/styles.css.d.ts +4 -0
- package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
- package/dist/components/ColorMode/useSystemScheme.d.ts +7 -0
- package/dist/components/ColorMode/useSystemScheme.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.d.ts +7 -2
- package/dist/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox/comboboxState.d.ts +29 -0
- package/dist/components/Combobox/comboboxState.d.ts.map +1 -0
- package/dist/components/Combobox/native.css.d.ts +27 -0
- package/dist/components/Combobox/native.css.d.ts.map +1 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts +6 -10
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts +12 -37
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerSnap.stories.d.ts +22 -0
- package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
- package/dist/components/Drawer/native.css.d.ts +59 -0
- package/dist/components/Drawer/native.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.css.d.ts +96 -0
- package/dist/components/Drawer/parts.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.d.ts +56 -0
- package/dist/components/Drawer/parts.d.ts.map +1 -0
- package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
- package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +16 -57
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/touchInput.d.ts +35 -0
- package/dist/components/Drawer/touchInput.d.ts.map +1 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
- package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
- package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
- package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
- package/dist/components/Form/FormSubmitContext.d.ts +8 -0
- package/dist/components/Form/FormSubmitContext.d.ts.map +1 -0
- package/dist/components/Listbox/native.css.d.ts +84 -0
- package/dist/components/Listbox/native.css.d.ts.map +1 -0
- package/dist/components/Menu/Menu.d.ts +7 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/native.css.d.ts +14 -0
- package/dist/components/Menu/native.css.d.ts.map +1 -0
- package/dist/components/Menu/useMenuOpen.d.ts +16 -0
- package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
- package/dist/components/Menu/useMenuRoot.d.ts +3 -6
- package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
- package/dist/components/Popover/native.css.d.ts +11 -0
- package/dist/components/Popover/native.css.d.ts.map +1 -0
- package/dist/components/Select/Select.d.ts +9 -2
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.stories.d.ts +11 -0
- package/dist/components/Select/Select.stories.d.ts.map +1 -1
- package/dist/components/Select/native.css.d.ts +11 -0
- package/dist/components/Select/native.css.d.ts.map +1 -0
- package/dist/components/Select/selectState.d.ts +21 -0
- package/dist/components/Select/selectState.d.ts.map +1 -0
- package/dist/components/Select/styles.css.d.ts +10 -0
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Select/useSelectRoot.d.ts +6 -8
- package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
- package/dist/components/SideNav/Chevron.d.ts +4 -0
- package/dist/components/SideNav/Chevron.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +6 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/SideNav/types.d.ts +6 -0
- package/dist/components/SideNav/types.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.d.ts +38 -0
- package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
- package/dist/components/SplitPane/styles.css.d.ts +78 -0
- package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
- package/dist/components/Tabs/TabLayout.d.ts +7 -0
- package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/indicator.d.ts +28 -0
- package/dist/components/Tabs/indicator.d.ts.map +1 -0
- package/dist/components/Tabs/styles.css.d.ts +4 -0
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
- package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
- package/dist/components/Tabs/useTabKeys.d.ts +13 -0
- package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
- package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
- package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +2 -24
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +8 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/Tag/types.d.ts +39 -0
- package/dist/components/Tag/types.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts +13 -0
- package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts +9 -2
- package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider/overrideRule.d.ts +7 -0
- package/dist/components/ThemeProvider/overrideRule.d.ts.map +1 -0
- package/dist/components/ThemeProvider/overrides.d.ts +16 -0
- package/dist/components/ThemeProvider/overrides.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.d.ts +2 -21
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/Toast/native.css.d.ts +81 -0
- package/dist/components/Toast/native.css.d.ts.map +1 -0
- package/dist/components/Toast/store.d.ts +47 -0
- package/dist/components/Toast/store.d.ts.map +1 -0
- package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
- package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +6 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
- package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.d.ts +33 -0
- package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
- package/dist/components/VirtualList/styles.css.d.ts +28 -0
- package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5090 -4381
- package/dist/index.js.map +1 -1
- package/dist/shared/ButtonHost.d.ts +11 -0
- package/dist/shared/ButtonHost.d.ts.map +1 -0
- package/dist/shared/pressableChild.d.ts +14 -0
- package/dist/shared/pressableChild.d.ts.map +1 -0
- package/dist/shared/textOf.d.ts +7 -0
- package/dist/shared/textOf.d.ts.map +1 -0
- package/dist/table.js +1 -1
- package/package.json +3 -2
- package/src/components/AppShell/AppShell.meta.ts +1 -1
- package/src/components/AppShell/AppShell.native.tsx +174 -0
- package/src/components/AppShell/native.css.ts +56 -0
- package/src/components/Button/Button.tsx +15 -4
- package/src/components/ColorMode/ColorMode.meta.ts +1 -1
- package/src/components/ColorMode/ColorMode.tsx +16 -37
- package/src/components/ColorMode/storage.native.ts +8 -0
- package/src/components/ColorMode/storage.ts +19 -0
- package/src/components/ColorMode/styles.css.ts +6 -0
- package/src/components/ColorMode/useSystemScheme.native.ts +6 -0
- package/src/components/ColorMode/useSystemScheme.ts +19 -0
- package/src/components/Combobox/Combobox.meta.ts +1 -1
- package/src/components/Combobox/Combobox.native.tsx +295 -0
- package/src/components/Combobox/Combobox.tsx +14 -8
- package/src/components/Combobox/comboboxState.ts +107 -0
- package/src/components/Combobox/native.css.ts +33 -0
- package/src/components/Combobox/useComboboxRoot.ts +17 -91
- package/src/components/Drawer/Drawer.meta.ts +2 -1
- package/src/components/Drawer/Drawer.native.tsx +491 -0
- package/src/components/Drawer/Drawer.tsx +65 -143
- package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
- package/src/components/Drawer/DrawerSnap.stories.tsx +225 -0
- package/src/components/Drawer/native.css.ts +62 -0
- package/src/components/Drawer/parts.css.ts +125 -0
- package/src/components/Drawer/parts.tsx +110 -0
- package/src/components/Drawer/sheetPhysics.ts +103 -0
- package/src/components/Drawer/styles.css.ts +18 -73
- package/src/components/Drawer/touchInput.ts +81 -0
- package/src/components/Drawer/useDrawerRoot.ts +90 -0
- package/src/components/Drawer/useSheetHeights.ts +42 -0
- package/src/components/Drawer/useSwipeDismiss.ts +146 -23
- package/src/components/Form/Form.meta.ts +1 -1
- package/src/components/Form/Form.native.tsx +120 -0
- package/src/components/Form/FormSubmitContext.ts +12 -0
- package/src/components/Listbox/Listbox.meta.ts +1 -1
- package/src/components/Listbox/Listbox.native.tsx +153 -0
- package/src/components/Listbox/native.css.ts +91 -0
- package/src/components/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.native.tsx +268 -0
- package/src/components/Menu/Menu.tsx +11 -2
- package/src/components/Menu/native.css.ts +18 -0
- package/src/components/Menu/useMenuOpen.ts +33 -0
- package/src/components/Menu/useMenuRoot.ts +7 -26
- package/src/components/Popover/Popover.meta.ts +1 -1
- package/src/components/Popover/Popover.native.tsx +205 -0
- package/src/components/Popover/native.css.ts +13 -0
- package/src/components/ScrollArea/ScrollArea.meta.ts +1 -1
- package/src/components/ScrollArea/ScrollArea.native.tsx +61 -0
- package/src/components/Select/Select.meta.ts +1 -1
- package/src/components/Select/Select.native.tsx +224 -0
- package/src/components/Select/Select.stories.tsx +49 -1
- package/src/components/Select/Select.tsx +20 -3
- package/src/components/Select/native.css.ts +13 -0
- package/src/components/Select/selectState.ts +36 -0
- package/src/components/Select/styles.css.ts +11 -0
- package/src/components/Select/useSelectRoot.ts +9 -30
- package/src/components/SideNav/Chevron.native.tsx +20 -0
- package/src/components/SideNav/Chevron.tsx +20 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -1
- package/src/components/SideNav/SideNav.tsx +57 -41
- package/src/components/SideNav/styles.css.ts +8 -0
- package/src/components/SideNav/types.ts +6 -0
- package/src/components/SplitPane/SplitPane.meta.ts +40 -0
- package/src/components/SplitPane/SplitPane.native.tsx +47 -0
- package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
- package/src/components/SplitPane/SplitPane.tsx +218 -0
- package/src/components/SplitPane/styles.css.ts +81 -0
- package/src/components/Tabs/TabLayout.native.tsx +9 -0
- package/src/components/Tabs/TabLayout.tsx +6 -0
- package/src/components/Tabs/Tabs.meta.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +38 -136
- package/src/components/Tabs/indicator.ts +36 -0
- package/src/components/Tabs/styles.css.ts +7 -0
- package/src/components/Tabs/useTabIndicator.native.ts +35 -0
- package/src/components/Tabs/useTabIndicator.ts +49 -0
- package/src/components/Tabs/useTabKeys.native.ts +2 -0
- package/src/components/Tabs/useTabKeys.ts +99 -0
- package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
- package/src/components/Tag/RemoveGlyph.tsx +10 -0
- package/src/components/Tag/Tag.meta.ts +1 -1
- package/src/components/Tag/Tag.native.tsx +70 -0
- package/src/components/Tag/Tag.tsx +9 -37
- package/src/components/Tag/styles.css.ts +9 -0
- package/src/components/Tag/types.ts +43 -0
- package/src/components/ThemeProvider/ThemeOverrides.stories.tsx +113 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -1
- package/src/components/ThemeProvider/ThemeProvider.native.tsx +36 -7
- package/src/components/ThemeProvider/ThemeProvider.tsx +42 -6
- package/src/components/ThemeProvider/overrideRule.ts +39 -0
- package/src/components/ThemeProvider/overrides.ts +27 -0
- package/src/components/Toast/Toast.meta.ts +1 -1
- package/src/components/Toast/ToastProvider.native.tsx +226 -0
- package/src/components/Toast/ToastProvider.tsx +22 -84
- package/src/components/Toast/native.css.ts +78 -0
- package/src/components/Toast/store.ts +96 -0
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/components/Tooltip/Tooltip.native.tsx +36 -0
- package/src/components/Tooltip/TooltipHint.ts +13 -0
- package/src/components/Tree/Tree.meta.ts +1 -1
- package/src/components/Tree/Tree.tsx +23 -135
- package/src/components/Tree/styles.css.ts +6 -0
- package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
- package/src/components/Tree/useTreeKeyboard.ts +152 -0
- package/src/components/VirtualList/VirtualList.meta.ts +39 -0
- package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
- package/src/components/VirtualList/VirtualList.tsx +103 -0
- package/src/components/VirtualList/styles.css.ts +31 -0
- package/src/form.native.ts +7 -0
- package/src/hooks/useContainerBelow.native.ts +30 -0
- package/src/index.native.ts +48 -0
- package/src/index.ts +9 -1
- package/src/shared/ButtonHost.native.tsx +70 -0
- package/src/shared/ButtonHost.tsx +12 -0
- package/src/shared/focusInput.native.ts +18 -0
- package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
- package/src/shared/pressableChild.ts +33 -0
- package/src/shared/textOf.ts +13 -0
- package/src/shared/useKeyboardHeight.native.ts +18 -0
- package/src/shared/useScreenReaderEnabled.native.ts +19 -0
- package/dist/Table-ColfpgKV.js +0 -1552
- package/dist/Table-ColfpgKV.js.map +0 -1
|
@@ -124,4 +124,15 @@ export const styles = css.create({
|
|
|
124
124
|
hidden: {
|
|
125
125
|
display: 'none',
|
|
126
126
|
},
|
|
127
|
+
// Popup's `empty`: no items to choose from.
|
|
128
|
+
empty: {
|
|
129
|
+
paddingTop: spacing.md,
|
|
130
|
+
paddingBottom: spacing.md,
|
|
131
|
+
paddingLeft: spacing.md,
|
|
132
|
+
paddingRight: spacing.md,
|
|
133
|
+
fontFamily: typography.fontFamily,
|
|
134
|
+
fontSize: typography.fontSizeSm,
|
|
135
|
+
color: colors.textMuted,
|
|
136
|
+
textAlign: 'center',
|
|
137
|
+
},
|
|
127
138
|
})
|
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import {useState, useCallback, useMemo, useRef, useId, useEffect} from 'react'
|
|
2
|
-
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
3
2
|
import type {SelectContextValue} from './SelectContext'
|
|
3
|
+
import {useSelectState, type SelectStateOptions} from './selectState'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function useSelectRoot({
|
|
13
|
-
defaultValue,
|
|
14
|
-
value: controlledValue,
|
|
15
|
-
onValueChange,
|
|
16
|
-
initialLabels,
|
|
17
|
-
}: UseSelectRootOptions) {
|
|
18
|
-
const [value, setValue] = useControllableValue<string | null>(
|
|
19
|
-
controlledValue,
|
|
20
|
-
defaultValue ?? null,
|
|
21
|
-
onValueChange,
|
|
22
|
-
)
|
|
23
|
-
const [open, setOpen] = useState(false)
|
|
24
|
-
const [labels, setLabels] = useState<Record<string, string>>(initialLabels ?? {})
|
|
5
|
+
/**
|
|
6
|
+
* The web adapter over the shared Select state: the highlighted option,
|
|
7
|
+
* document order, keys and DOM focus (the WAI-ARIA select-only combobox).
|
|
8
|
+
*/
|
|
9
|
+
export function useSelectRoot(options: SelectStateOptions) {
|
|
10
|
+
const {value, setValue, open, setOpen, labels, registerLabel} = useSelectState(options)
|
|
25
11
|
const [highlightedId, setHighlightedId] = useState<string | null>(null)
|
|
26
12
|
const listboxId = useId()
|
|
27
13
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
@@ -33,7 +19,7 @@ export function useSelectRoot({
|
|
|
33
19
|
setOpen(false)
|
|
34
20
|
setHighlightedId(null)
|
|
35
21
|
triggerRef.current?.focus()
|
|
36
|
-
}, [])
|
|
22
|
+
}, [setOpen])
|
|
37
23
|
|
|
38
24
|
const toggle = useCallback(() => {
|
|
39
25
|
setOpen((prev) => {
|
|
@@ -54,14 +40,7 @@ export function useSelectRoot({
|
|
|
54
40
|
}
|
|
55
41
|
return !prev
|
|
56
42
|
})
|
|
57
|
-
}, [value])
|
|
58
|
-
|
|
59
|
-
const registerLabel = useCallback((v: string, label: string) => {
|
|
60
|
-
setLabels((prev) => {
|
|
61
|
-
if (prev[v] === label) return prev
|
|
62
|
-
return {...prev, [v]: label}
|
|
63
|
-
})
|
|
64
|
-
}, [])
|
|
43
|
+
}, [value, setOpen])
|
|
65
44
|
|
|
66
45
|
const registerItem = useCallback((id: string, itemValue: string, element: HTMLElement) => {
|
|
67
46
|
itemsRef.current.set(id, {value: itemValue, element})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Svg, {Path} from 'react-native-svg'
|
|
2
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
3
|
+
import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
|
|
4
|
+
import type {ChevronProps} from './types'
|
|
5
|
+
|
|
6
|
+
/** The web's chevron, drawn with react-native-svg in the trigger's resolved colour. */
|
|
7
|
+
export function Chevron({color}: ChevronProps) {
|
|
8
|
+
const stroke = useResolvedColors()[color]
|
|
9
|
+
return (
|
|
10
|
+
<Svg width={SIZES_PX.glyphXs} height={SIZES_PX.glyphXs} viewBox="0 0 24 24" fill="none">
|
|
11
|
+
<Path
|
|
12
|
+
d="M9 18l6-6-6-6"
|
|
13
|
+
stroke={stroke}
|
|
14
|
+
strokeWidth={2.5}
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
/>
|
|
18
|
+
</Svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
2
|
+
import type {ChevronProps} from './types'
|
|
3
|
+
|
|
4
|
+
/** The group disclosure's chevron, drawn in the trigger's text colour. */
|
|
5
|
+
export function Chevron(_props: ChevronProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
width={SIZES_PX.glyphXs}
|
|
9
|
+
height={SIZES_PX.glyphXs}
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth={2.5}
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
>
|
|
17
|
+
<path d="M9 18l6-6-6-6" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required.',
|
|
5
|
+
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required. On React Native the same parts render, and a Group knows whether it holds the active item from its Items, not from the DOM.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'App-level sidebar navigation — flat `Section`s, every destination visible',
|
|
8
8
|
'Settings navigation with labelled regions',
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
} from 'react'
|
|
2
11
|
import {html} from 'react-strict-dom'
|
|
3
12
|
import {styles} from './styles.css'
|
|
4
13
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
5
14
|
import {SideNavContext, useSideNav} from './SideNavContext'
|
|
15
|
+
import {isNative} from '../../platform'
|
|
16
|
+
import {Chevron} from './Chevron'
|
|
17
|
+
|
|
18
|
+
/** A Group's own items: each Item inside it registers its value here. */
|
|
19
|
+
const GroupItemsContext = createContext<((value: string) => () => void) | null>(null)
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* SideNav has two ways to chunk a rail, and they are NOT interchangeable.
|
|
@@ -130,39 +144,40 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
130
144
|
const key = groupKey ?? label
|
|
131
145
|
const {expandedGroups, toggleGroup, expandGroup, activeValue} = useSideNav()
|
|
132
146
|
const isExpanded = expandedGroups.has(key)
|
|
133
|
-
const groupRef = useRef<HTMLDivElement>(null)
|
|
134
147
|
const triggerId = useId()
|
|
135
148
|
const panelId = useId()
|
|
149
|
+
// The values of the Items inside, from props and state rather than a DOM
|
|
150
|
+
// query, so both platforms know whether the active item is in here.
|
|
151
|
+
const [values, setValues] = useState<ReadonlySet<string>>(() => new Set())
|
|
152
|
+
const registerValue = useCallback((value: string) => {
|
|
153
|
+
setValues((prev) => (prev.has(value) ? prev : new Set(prev).add(value)))
|
|
154
|
+
return () =>
|
|
155
|
+
setValues((prev) => {
|
|
156
|
+
if (!prev.has(value)) return prev
|
|
157
|
+
const next = new Set(prev)
|
|
158
|
+
next.delete(value)
|
|
159
|
+
return next
|
|
160
|
+
})
|
|
161
|
+
}, [])
|
|
162
|
+
const hasActiveChild = activeValue !== null && values.has(activeValue)
|
|
136
163
|
|
|
137
164
|
// Auto-expand if this group contains the active item. Uses expandGroup (not
|
|
138
165
|
// toggleGroup) so it never races the defaultExpanded effect into a closed
|
|
139
166
|
// state when the active item lives in a defaultExpanded group.
|
|
140
167
|
useEffect(() => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (!el) return
|
|
144
|
-
const activeBtn = el.querySelector(`[data-nav-value="${activeValue}"]`)
|
|
145
|
-
if (activeBtn) {
|
|
146
|
-
expandGroup(key)
|
|
147
|
-
}
|
|
148
|
-
}, [activeValue, key, expandedGroups, expandGroup])
|
|
168
|
+
if (hasActiveChild && !expandedGroups.has(key)) expandGroup(key)
|
|
169
|
+
}, [hasActiveChild, key, expandedGroups, expandGroup])
|
|
149
170
|
|
|
150
|
-
// Expand on
|
|
171
|
+
// Expand once, on mount, if defaultExpanded (idempotent add, not a toggle).
|
|
172
|
+
const appliedDefault = useRef(false)
|
|
151
173
|
useEffect(() => {
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
157
|
-
}, [])
|
|
158
|
-
|
|
159
|
-
const hasActiveChild = (() => {
|
|
160
|
-
if (!activeValue || !groupRef.current) return false
|
|
161
|
-
return !!groupRef.current.querySelector(`[data-nav-value="${activeValue}"]`)
|
|
162
|
-
})()
|
|
174
|
+
if (appliedDefault.current) return
|
|
175
|
+
appliedDefault.current = true
|
|
176
|
+
if (defaultExpanded) expandGroup(key)
|
|
177
|
+
}, [defaultExpanded, expandGroup, key])
|
|
163
178
|
|
|
164
179
|
return (
|
|
165
|
-
<html.div
|
|
180
|
+
<html.div style={styles.group}>
|
|
166
181
|
<html.button
|
|
167
182
|
type="button"
|
|
168
183
|
id={triggerId}
|
|
@@ -171,27 +186,26 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
171
186
|
aria-expanded={isExpanded}
|
|
172
187
|
aria-controls={isExpanded ? panelId : undefined}
|
|
173
188
|
>
|
|
174
|
-
<html.span
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
viewBox="0 0 24 24"
|
|
179
|
-
fill="none"
|
|
180
|
-
stroke="currentColor"
|
|
181
|
-
strokeWidth={2.5}
|
|
182
|
-
strokeLinecap="round"
|
|
183
|
-
strokeLinejoin="round"
|
|
184
|
-
>
|
|
185
|
-
<path d="M9 18l6-6-6-6" />
|
|
186
|
-
</svg>
|
|
189
|
+
<html.span
|
|
190
|
+
style={[styles.chevron, isNative && styles.nativeFlex, isExpanded && styles.chevronOpen]}
|
|
191
|
+
>
|
|
192
|
+
<Chevron color={hasActiveChild ? 'text' : 'textMuted'} />
|
|
187
193
|
</html.span>
|
|
188
|
-
{
|
|
194
|
+
{/* Native refuses bare text beside the chevron's view; the web keeps it bare. */}
|
|
195
|
+
{isNative ? <html.span>{label}</html.span> : label}
|
|
189
196
|
</html.button>
|
|
190
|
-
{
|
|
191
|
-
|
|
197
|
+
{/* Kept mounted while collapsed, so its Items stay registered with the group. */}
|
|
198
|
+
<GroupItemsContext.Provider value={registerValue}>
|
|
199
|
+
<html.div
|
|
200
|
+
id={panelId}
|
|
201
|
+
role="group"
|
|
202
|
+
aria-labelledby={triggerId}
|
|
203
|
+
hidden={isExpanded ? undefined : true}
|
|
204
|
+
style={[styles.items, !isExpanded && styles.itemsHidden]}
|
|
205
|
+
>
|
|
192
206
|
{children}
|
|
193
207
|
</html.div>
|
|
194
|
-
|
|
208
|
+
</GroupItemsContext.Provider>
|
|
195
209
|
</html.div>
|
|
196
210
|
)
|
|
197
211
|
}
|
|
@@ -239,11 +253,13 @@ interface ItemProps {
|
|
|
239
253
|
|
|
240
254
|
function Item({value, children, icon}: ItemProps) {
|
|
241
255
|
const {activeValue, onSelect, registerItem} = useSideNav()
|
|
256
|
+
const registerInGroup = useContext(GroupItemsContext)
|
|
242
257
|
const isActive = activeValue === value
|
|
243
258
|
|
|
244
259
|
useEffect(() => {
|
|
245
260
|
return registerItem(value)
|
|
246
261
|
}, [value, registerItem])
|
|
262
|
+
useEffect(() => registerInGroup?.(value), [value, registerInGroup])
|
|
247
263
|
|
|
248
264
|
return (
|
|
249
265
|
<html.button
|
|
@@ -255,7 +271,7 @@ function Item({value, children, icon}: ItemProps) {
|
|
|
255
271
|
>
|
|
256
272
|
<html.span aria-hidden style={[styles.marker, isActive && styles.markerActive]} />
|
|
257
273
|
{icon ? (
|
|
258
|
-
<html.span aria-hidden style={styles.itemIcon}>
|
|
274
|
+
<html.span aria-hidden style={[styles.itemIcon, isNative && styles.nativeFlex]}>
|
|
259
275
|
{icon}
|
|
260
276
|
</html.span>
|
|
261
277
|
) : null}
|
|
@@ -137,6 +137,14 @@ export const styles = css.create({
|
|
|
137
137
|
transitionDuration: duration.fast,
|
|
138
138
|
transitionTimingFunction: easing.standard,
|
|
139
139
|
},
|
|
140
|
+
// A collapsed group's items stay mounted (they tell the group what it holds), unseen.
|
|
141
|
+
itemsHidden: {
|
|
142
|
+
display: 'none',
|
|
143
|
+
},
|
|
144
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
145
|
+
nativeFlex: {
|
|
146
|
+
display: 'flex',
|
|
147
|
+
},
|
|
140
148
|
markerActive: {
|
|
141
149
|
height: sizes.navMarkerH,
|
|
142
150
|
},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Two panes side by side with a resizable separator: the first Pane is sized by the Handle (pointer drag, arrow keys, Shift for larger steps, Home and End), the second takes the rest. Min, max and starting widths are size tokens. Below collapseBelow, measured on its own width, the panes stack in one column and the handle goes; on native they always stack. The Handle is a hairline that thickens to the accent on hover, keyboard focus or drag.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'A list beside its detail where people need more room on either side (a session list and its transcript)',
|
|
8
|
+
'An editor beside its preview',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'A fixed list/detail layout nobody resizes — use Grid layout="split"',
|
|
12
|
+
'Navigation beside content — use AppShell or the page-with-sidenav recipe',
|
|
13
|
+
'A short-lived inspection over a list — use DetailPanel',
|
|
14
|
+
],
|
|
15
|
+
anatomy: {
|
|
16
|
+
required: ['Root', 'Pane'],
|
|
17
|
+
optional: ['Handle'],
|
|
18
|
+
},
|
|
19
|
+
relatedTo: [
|
|
20
|
+
{
|
|
21
|
+
component: 'Grid',
|
|
22
|
+
kind: 'contrast',
|
|
23
|
+
relationship: 'Grid layout="split" is a fixed split; SplitPane lets people resize it',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
component: 'DetailPanel',
|
|
27
|
+
kind: 'contrast',
|
|
28
|
+
relationship: 'DetailPanel overlays the list; SplitPane keeps both side by side',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
example: `<SplitPane.Root aria-label="Resize the session list" minSize="sidebarW" maxSize="panelLg">
|
|
32
|
+
<SplitPane.Pane>
|
|
33
|
+
<List.Root aria-label="Sessions">…</List.Root>
|
|
34
|
+
</SplitPane.Pane>
|
|
35
|
+
<SplitPane.Handle />
|
|
36
|
+
<SplitPane.Pane>
|
|
37
|
+
<Panel.Root>…</Panel.Root>
|
|
38
|
+
</SplitPane.Pane>
|
|
39
|
+
</SplitPane.Root>`,
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import type {SizeToken} from '@duro-app/tokens/keys'
|
|
4
|
+
import type {Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
5
|
+
import {styles} from './styles.css'
|
|
6
|
+
|
|
7
|
+
interface RootProps {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
/** Web only: on a phone the panes stack. */
|
|
10
|
+
defaultSize?: SizeToken
|
|
11
|
+
/** Web only. */
|
|
12
|
+
size?: number
|
|
13
|
+
/** Web only. */
|
|
14
|
+
onSizeChange?: (px: number) => void
|
|
15
|
+
/** Web only. */
|
|
16
|
+
minSize?: SizeToken
|
|
17
|
+
/** Web only. */
|
|
18
|
+
maxSize?: SizeToken
|
|
19
|
+
/** Web only: a phone is below every breakpoint, so native always stacks. */
|
|
20
|
+
collapseBelow?: Breakpoint
|
|
21
|
+
/** The handle's accessible name (web only: native has no handle). */
|
|
22
|
+
'aria-label': string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Native SplitPane (plan N0, Slice B): a phone shows one pane at a time's
|
|
27
|
+
* worth of width, so the panes stack in one column and there is no handle —
|
|
28
|
+
* the shape the web takes below `collapseBelow`.
|
|
29
|
+
*/
|
|
30
|
+
function Root({children}: RootProps) {
|
|
31
|
+
return <html.div style={[styles.root, styles.rootStacked]}>{children}</html.div>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Pane({children}: {children: ReactNode}) {
|
|
35
|
+
return <html.div style={styles.stackedPane}>{children}</html.div>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** No handle on native. */
|
|
39
|
+
function Handle() {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SplitPane = {
|
|
44
|
+
Root,
|
|
45
|
+
Pane,
|
|
46
|
+
Handle,
|
|
47
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect, fn, userEvent, waitFor} from 'storybook/test'
|
|
3
|
+
import {css, html} from 'react-strict-dom'
|
|
4
|
+
import {SIZES_PX, SPACING_PX, type SizeToken} from '@duro-app/tokens/keys'
|
|
5
|
+
import type {Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
6
|
+
import {SplitPane} from './SplitPane'
|
|
7
|
+
import {setViewport} from '../Drawer/touchInput'
|
|
8
|
+
import {List} from '../List/List'
|
|
9
|
+
import {Panel} from '../Panel/Panel'
|
|
10
|
+
import {Heading} from '../Heading/Heading'
|
|
11
|
+
import {Text} from '../Text/Text'
|
|
12
|
+
|
|
13
|
+
const meta: Meta = {
|
|
14
|
+
title: 'Components/SplitPane',
|
|
15
|
+
parameters: {layout: 'fullscreen'},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj
|
|
20
|
+
|
|
21
|
+
const SESSIONS = ['Refactor auth', 'Flaky e2e', 'Release notes', 'Docs pass', 'Perf trace']
|
|
22
|
+
|
|
23
|
+
// A SplitPane fills its parent: the story gives it the viewport's height.
|
|
24
|
+
const frame = css.create({fill: {height: '100vh'}})
|
|
25
|
+
|
|
26
|
+
interface SplitArgs {
|
|
27
|
+
defaultSize: SizeToken
|
|
28
|
+
minSize: SizeToken
|
|
29
|
+
maxSize: SizeToken
|
|
30
|
+
collapseBelow: Breakpoint
|
|
31
|
+
onSizeChange?: (px: number) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_ARGS: SplitArgs = {
|
|
35
|
+
defaultSize: 'sidebarW',
|
|
36
|
+
minSize: 'sidebarW',
|
|
37
|
+
maxSize: 'panelLg',
|
|
38
|
+
collapseBelow: 'sm',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Width tokens that make sense for a pane, smallest first.
|
|
42
|
+
const PANE_SIZES: SizeToken[] = ['sidebarW', 'panelSm', 'panelMd', 'panelLg']
|
|
43
|
+
|
|
44
|
+
/** The picked B0 direction (docs/mockups/split-pane/Hairline.dc.html). */
|
|
45
|
+
function Split(props: Partial<SplitArgs>) {
|
|
46
|
+
const {defaultSize, minSize, maxSize, collapseBelow, onSizeChange} = {...DEFAULT_ARGS, ...props}
|
|
47
|
+
return (
|
|
48
|
+
<html.div style={frame.fill}>
|
|
49
|
+
<SplitPane.Root
|
|
50
|
+
aria-label="Resize the session list"
|
|
51
|
+
defaultSize={defaultSize}
|
|
52
|
+
minSize={minSize}
|
|
53
|
+
maxSize={maxSize}
|
|
54
|
+
collapseBelow={collapseBelow}
|
|
55
|
+
onSizeChange={onSizeChange}
|
|
56
|
+
>
|
|
57
|
+
<SplitPane.Pane>
|
|
58
|
+
<List.Root aria-label="Sessions">
|
|
59
|
+
{SESSIONS.map((s) => (
|
|
60
|
+
<List.Item key={s}>
|
|
61
|
+
<List.Text>{s}</List.Text>
|
|
62
|
+
</List.Item>
|
|
63
|
+
))}
|
|
64
|
+
</List.Root>
|
|
65
|
+
</SplitPane.Pane>
|
|
66
|
+
<SplitPane.Handle />
|
|
67
|
+
<SplitPane.Pane>
|
|
68
|
+
<Panel.Root>
|
|
69
|
+
<Panel.Header>
|
|
70
|
+
<Heading level={2}>Refactor auth</Heading>
|
|
71
|
+
</Panel.Header>
|
|
72
|
+
<Panel.Body>
|
|
73
|
+
<Text>The transcript of the selected session.</Text>
|
|
74
|
+
</Panel.Body>
|
|
75
|
+
</Panel.Root>
|
|
76
|
+
</SplitPane.Pane>
|
|
77
|
+
</SplitPane.Root>
|
|
78
|
+
</html.div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const Default: StoryObj<SplitArgs> = {
|
|
83
|
+
args: {...DEFAULT_ARGS, onSizeChange: fn()},
|
|
84
|
+
argTypes: {
|
|
85
|
+
defaultSize: {
|
|
86
|
+
control: 'select',
|
|
87
|
+
options: PANE_SIZES,
|
|
88
|
+
description: 'Starting width of the first pane',
|
|
89
|
+
},
|
|
90
|
+
minSize: {control: 'select', options: PANE_SIZES, description: 'Narrowest the first pane goes'},
|
|
91
|
+
maxSize: {control: 'select', options: PANE_SIZES, description: 'Widest the first pane goes'},
|
|
92
|
+
collapseBelow: {
|
|
93
|
+
control: 'select',
|
|
94
|
+
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
95
|
+
description: "Below this breakpoint (the split's own width) the panes stack",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
// Keyed by the starting width: it seeds state on mount, so a change remounts the split.
|
|
99
|
+
render: (args) => <Split key={args.defaultSize} {...args} />,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const separator = () => document.querySelector<HTMLElement>('[role="separator"]')
|
|
103
|
+
|
|
104
|
+
export const KeyboardMovesTheSeparator: Story = {
|
|
105
|
+
render: () => <Split />,
|
|
106
|
+
beforeEach: async () => {
|
|
107
|
+
await setViewport(1200, 800)
|
|
108
|
+
},
|
|
109
|
+
play: async () => {
|
|
110
|
+
await waitFor(() => expect(separator()).not.toBeNull())
|
|
111
|
+
const handle = separator()!
|
|
112
|
+
const primary = document.getElementById(handle.getAttribute('aria-controls')!)!
|
|
113
|
+
await expect(handle).toHaveAttribute('aria-valuenow', String(SIZES_PX.sidebarW))
|
|
114
|
+
await expect(handle).toHaveAttribute('aria-valuemin', String(SIZES_PX.sidebarW))
|
|
115
|
+
await expect(handle).toHaveAttribute('aria-valuemax', String(SIZES_PX.panelLg))
|
|
116
|
+
|
|
117
|
+
handle.focus()
|
|
118
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
119
|
+
const after = SIZES_PX.sidebarW + SPACING_PX.md
|
|
120
|
+
await expect(handle).toHaveAttribute('aria-valuenow', String(after))
|
|
121
|
+
await waitFor(() => expect(primary.getBoundingClientRect().width).toBeCloseTo(after, 0))
|
|
122
|
+
|
|
123
|
+
await userEvent.keyboard('{End}')
|
|
124
|
+
await expect(handle).toHaveAttribute('aria-valuenow', String(SIZES_PX.panelLg))
|
|
125
|
+
// the bound holds
|
|
126
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
127
|
+
await expect(handle).toHaveAttribute('aria-valuenow', String(SIZES_PX.panelLg))
|
|
128
|
+
|
|
129
|
+
await userEvent.keyboard('{Home}')
|
|
130
|
+
await expect(handle).toHaveAttribute('aria-valuenow', String(SIZES_PX.sidebarW))
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const NarrowStacksInOneColumn: Story = {
|
|
135
|
+
render: () => <Split />,
|
|
136
|
+
beforeEach: async () => {
|
|
137
|
+
await setViewport(390, 844)
|
|
138
|
+
},
|
|
139
|
+
play: async ({canvas}) => {
|
|
140
|
+
await waitFor(() => expect(separator()).toBeNull())
|
|
141
|
+
const list = canvas.getByRole('list', {name: 'Sessions'})
|
|
142
|
+
const heading = canvas.getByRole('heading', {name: 'Refactor auth'})
|
|
143
|
+
// one column: the detail sits below the list
|
|
144
|
+
await expect(heading.getBoundingClientRect().top).toBeGreaterThan(
|
|
145
|
+
list.getBoundingClientRect().bottom - 1,
|
|
146
|
+
)
|
|
147
|
+
},
|
|
148
|
+
}
|