@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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
type Ref,
|
|
5
|
+
type RefObject,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useImperativeHandle,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import {Pressable} from 'react-native'
|
|
15
|
+
import {html} from 'react-strict-dom'
|
|
16
|
+
import {styles} from './styles.css'
|
|
17
|
+
import {nativeStyles} from './native.css'
|
|
18
|
+
import type {PopoverAlign, PopoverSide} from './position'
|
|
19
|
+
import {AnchoredOverlay} from '../../shared/portal/AnchoredOverlay'
|
|
20
|
+
import {focusForAccessibility} from '../../shared/accessibilityFocus'
|
|
21
|
+
import {mergeRefs} from '../../shared/mergeRefs'
|
|
22
|
+
import {ModalContext} from '../../shared/ModalContext'
|
|
23
|
+
import type {ControlSize} from '../../shared/types'
|
|
24
|
+
import {useTooltipHint} from '../Tooltip/TooltipHint'
|
|
25
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
26
|
+
|
|
27
|
+
interface PopoverContextValue {
|
|
28
|
+
open: boolean
|
|
29
|
+
setOpen: (open: boolean) => void
|
|
30
|
+
popoverId: string
|
|
31
|
+
side: PopoverSide
|
|
32
|
+
align: PopoverAlign
|
|
33
|
+
triggerRef: RefObject<unknown>
|
|
34
|
+
/** Bumped by `reposition()`: the popup measures its trigger again */
|
|
35
|
+
placement: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PopoverContext = createContext<PopoverContextValue | null>(null)
|
|
39
|
+
|
|
40
|
+
function usePopover(part: string) {
|
|
41
|
+
const ctx = useContext(PopoverContext)
|
|
42
|
+
if (!ctx) throw new Error(`Popover.${part} must be used within Popover.Root`)
|
|
43
|
+
return ctx
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PopoverHandle {
|
|
47
|
+
/** Place the popup against its trigger again. */
|
|
48
|
+
reposition: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RootProps {
|
|
52
|
+
children: ReactNode
|
|
53
|
+
open?: boolean
|
|
54
|
+
defaultOpen?: boolean
|
|
55
|
+
onOpenChange?: (open: boolean) => void
|
|
56
|
+
side?: PopoverSide
|
|
57
|
+
align?: PopoverAlign
|
|
58
|
+
/** Web only: native places the popup against Popover.Trigger. */
|
|
59
|
+
anchor?: () => DOMRect
|
|
60
|
+
/** Web only: on native the popup is modal, so nothing outside it is pressed. */
|
|
61
|
+
ignore?: ReadonlyArray<unknown>
|
|
62
|
+
ref?: Ref<PopoverHandle>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Native Popover (plan N0, Slice B): the popup is placed against the trigger
|
|
67
|
+
* in an OverlayModal, so it is modal on native where the web's is not. A
|
|
68
|
+
* press outside or Android's back button closes it; the screen reader's
|
|
69
|
+
* focus moves into it on open and back to the trigger on close.
|
|
70
|
+
*/
|
|
71
|
+
function Root({
|
|
72
|
+
children,
|
|
73
|
+
open: openProp,
|
|
74
|
+
defaultOpen = false,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
side = 'bottom',
|
|
77
|
+
align = 'start',
|
|
78
|
+
ref,
|
|
79
|
+
}: RootProps) {
|
|
80
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
81
|
+
const controlled = openProp !== undefined
|
|
82
|
+
const open = controlled ? openProp : inner
|
|
83
|
+
const popoverId = useId()
|
|
84
|
+
const triggerRef = useRef<unknown>(null)
|
|
85
|
+
const [placement, setPlacement] = useState(0)
|
|
86
|
+
const openRef = useRef(open)
|
|
87
|
+
openRef.current = open
|
|
88
|
+
|
|
89
|
+
const setOpen = useCallback(
|
|
90
|
+
(next: boolean) => {
|
|
91
|
+
if (!controlled) setInner(next)
|
|
92
|
+
if (next !== openRef.current) onOpenChange?.(next)
|
|
93
|
+
},
|
|
94
|
+
[controlled, onOpenChange],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
useImperativeHandle(ref, () => ({reposition: () => setPlacement((n) => n + 1)}), [])
|
|
98
|
+
|
|
99
|
+
const wasOpen = useRef(open)
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (wasOpen.current && !open) focusForAccessibility(triggerRef.current, 'Popover')
|
|
102
|
+
wasOpen.current = open
|
|
103
|
+
}, [open])
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<PopoverContext.Provider value={{open, setOpen, popoverId, side, align, triggerRef, placement}}>
|
|
107
|
+
{children}
|
|
108
|
+
</PopoverContext.Provider>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface TriggerProps {
|
|
113
|
+
children: ReactNode
|
|
114
|
+
/** Accessible name: required when the label is an icon only. */
|
|
115
|
+
'aria-label'?: string
|
|
116
|
+
/** `small` matches a small Toggle or Button beside it. Default: `default`. */
|
|
117
|
+
size?: ControlSize
|
|
118
|
+
/** The trigger's host, e.g. to measure it. */
|
|
119
|
+
ref?: Ref<unknown>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* An RN Pressable host, so it carries an enclosing Tooltip's text as its hint.
|
|
124
|
+
*
|
|
125
|
+
* holds-until: react-strict-dom maps aria-description to accessibilityHint
|
|
126
|
+
* (https://github.com/react/react-strict-dom/issues/524).
|
|
127
|
+
*/
|
|
128
|
+
function Trigger({children, 'aria-label': ariaLabel, size = 'default', ref}: TriggerProps) {
|
|
129
|
+
const {open, setOpen, triggerRef} = usePopover('Trigger')
|
|
130
|
+
const hint = useTooltipHint()
|
|
131
|
+
return (
|
|
132
|
+
<Pressable
|
|
133
|
+
ref={mergeRefs(triggerRef, ref) as Ref<never>}
|
|
134
|
+
onPress={() => setOpen(!open)}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityLabel={ariaLabel}
|
|
137
|
+
accessibilityHint={hint}
|
|
138
|
+
accessibilityState={{expanded: open}}
|
|
139
|
+
>
|
|
140
|
+
<html.div
|
|
141
|
+
style={[styles.trigger, nativeStyles.nativeFlex, size === 'small' && styles.triggerSmall]}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</html.div>
|
|
145
|
+
</Pressable>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface PopupProps {
|
|
150
|
+
children: ReactNode
|
|
151
|
+
/** Accessible name for the dialog. */
|
|
152
|
+
label: string
|
|
153
|
+
/** Web only: on native every popup is its own Modal, above what opened it. */
|
|
154
|
+
raised?: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Popup({children, label}: PopupProps) {
|
|
158
|
+
const {open, setOpen, popoverId, side, align, triggerRef, placement} = usePopover('Popup')
|
|
159
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
160
|
+
return (
|
|
161
|
+
<AnchoredOverlay
|
|
162
|
+
key={placement}
|
|
163
|
+
visible={open}
|
|
164
|
+
anchorRef={triggerRef}
|
|
165
|
+
side={side}
|
|
166
|
+
align={align}
|
|
167
|
+
onRequestClose={() => setOpen(false)}
|
|
168
|
+
onShow={() => focusForAccessibility(popupRef.current, 'Popover')}
|
|
169
|
+
>
|
|
170
|
+
<html.div
|
|
171
|
+
ref={popupRef}
|
|
172
|
+
id={popoverId}
|
|
173
|
+
role="dialog"
|
|
174
|
+
aria-modal
|
|
175
|
+
aria-label={label}
|
|
176
|
+
style={[styles.popup, nativeStyles.popupInPlace]}
|
|
177
|
+
>
|
|
178
|
+
{/* Modal on native: a Popover or Menu opened inside it stacks above it. */}
|
|
179
|
+
<ModalContext.Provider value={true}>
|
|
180
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
181
|
+
</ModalContext.Provider>
|
|
182
|
+
</html.div>
|
|
183
|
+
</AnchoredOverlay>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Close({children}: {children: ReactNode}) {
|
|
188
|
+
const {setOpen} = usePopover('Close')
|
|
189
|
+
return (
|
|
190
|
+
<html.button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => setOpen(false)}
|
|
193
|
+
style={[styles.close, nativeStyles.nativeFlex]}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</html.button>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const Popover = {
|
|
201
|
+
Root,
|
|
202
|
+
Trigger,
|
|
203
|
+
Popup,
|
|
204
|
+
Close,
|
|
205
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
|
|
3
|
+
/** Native Popover additions to the shared styles. */
|
|
4
|
+
export const nativeStyles = css.create({
|
|
5
|
+
// The AnchoredOverlay's view places the popup; it flows inside it.
|
|
6
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
7
|
+
nativeFlex: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
},
|
|
10
|
+
popupInPlace: {
|
|
11
|
+
position: 'relative',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required. Inside a Drawer, a drag in the viewport scrolls it; the drawer only takes a drag toward its edge from the scroll start.',
|
|
5
|
+
'Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required. Inside a Drawer, a drag in the viewport scrolls it; the drawer only takes a drag toward its edge from the scroll start. On React Native the Viewport is an RN ScrollView with the platform scroll indicator, capped by the same maxHeight token key; Scrollbar and Thumb render nothing.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Custom-styled scrollbars for content overflow areas',
|
|
8
8
|
'Chat logs, code blocks, or long lists with constrained height',
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {ComponentProps, ReactNode} from 'react'
|
|
2
|
+
import {ScrollView, StyleSheet} from 'react-native'
|
|
3
|
+
import {html} from 'react-strict-dom'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
import {resolveLength, type Length} from '../../shared/length'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Native ScrollArea (plan N0, Slice C): Viewport is an RN ScrollView with the
|
|
9
|
+
* platform's own scroll indicator, so Scrollbar and Thumb draw nothing. The
|
|
10
|
+
* cap (`maxHeight`, a size-token key) sits on the view around it, where
|
|
11
|
+
* react-strict-dom resolves the token.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function Root({children}: {children: ReactNode}) {
|
|
15
|
+
return <html.div style={styles.root}>{children}</html.div>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ViewportProps {
|
|
19
|
+
children: ReactNode
|
|
20
|
+
maxHeight?: Length
|
|
21
|
+
/** Extra styles for the viewport (e.g. a pointer-aware max height). */
|
|
22
|
+
style?: ComponentProps<typeof html.div>['style']
|
|
23
|
+
/** The web's roving-focus flag; native has no tab stops. */
|
|
24
|
+
rovingOwner?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Viewport({children, maxHeight, style}: ViewportProps) {
|
|
28
|
+
return (
|
|
29
|
+
<html.div
|
|
30
|
+
style={[maxHeight != null && styles.viewportMaxHeight(resolveLength(maxHeight)), style]}
|
|
31
|
+
>
|
|
32
|
+
<ScrollView style={layout.scroll}>{children}</ScrollView>
|
|
33
|
+
</html.div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Content({children}: {children: ReactNode}) {
|
|
38
|
+
return <html.div>{children}</html.div>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ScrollbarOrientation = 'vertical' | 'horizontal'
|
|
42
|
+
|
|
43
|
+
/** The platform draws its own scroll indicator. */
|
|
44
|
+
function Scrollbar(_props: {orientation?: ScrollbarOrientation; children?: ReactNode}) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Thumb(_props: {orientation?: ScrollbarOrientation}) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The ScrollView shrinks to its capped parent instead of growing past it.
|
|
53
|
+
const layout = StyleSheet.create({scroll: {flexGrow: 0, flexShrink: 1}})
|
|
54
|
+
|
|
55
|
+
export const ScrollArea = {
|
|
56
|
+
Root,
|
|
57
|
+
Viewport,
|
|
58
|
+
Content,
|
|
59
|
+
Scrollbar,
|
|
60
|
+
Thumb,
|
|
61
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it). Select.Trigger takes size="small" to match small Toggles and Buttons in a toolbar.',
|
|
5
|
+
'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it). Select.Trigger takes size="small" to match small Toggles and Buttons in a toolbar. Select.Popup takes label (the list\'s accessible name) and empty (shown in place of the list when it has no items). On React Native the trigger opens a bottom sheet (a Drawer at the medium and large detents) titled by label, with a Cancel text button (cancelLabel); the chosen option is filled with a trailing check, and a press picks an option and closes the sheet. Native has no arrow keys: each option is its own screen-reader stop.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'User must pick one option from a predefined list',
|
|
8
8
|
'Form field for country, category, role selection',
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
type RefObject,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useRef,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import {Pressable} from 'react-native'
|
|
12
|
+
import {html} from 'react-strict-dom'
|
|
13
|
+
import {styles} from './styles.css'
|
|
14
|
+
import {useSelectState} from './selectState'
|
|
15
|
+
import {Drawer} from '../Drawer/Drawer'
|
|
16
|
+
import {Listbox} from '../Listbox/Listbox'
|
|
17
|
+
import {Button} from '../Button/Button'
|
|
18
|
+
import {nativeStyles} from './native.css'
|
|
19
|
+
import {Chevron} from '../SideNav/Chevron'
|
|
20
|
+
import {useTooltipHint} from '../Tooltip/TooltipHint'
|
|
21
|
+
import {focusForAccessibility} from '../../shared/accessibilityFocus'
|
|
22
|
+
import {textOf} from '../../shared/textOf'
|
|
23
|
+
import type {ControlSize} from '../../shared/types'
|
|
24
|
+
|
|
25
|
+
/** The picker sheet's detents (plan N0, Decision 5). */
|
|
26
|
+
const SNAP_POINTS = ['medium', 'large'] as const
|
|
27
|
+
|
|
28
|
+
interface NativeSelectContextValue {
|
|
29
|
+
value: string | null
|
|
30
|
+
open: boolean
|
|
31
|
+
setOpen: (open: boolean) => void
|
|
32
|
+
choose: (value: string) => void
|
|
33
|
+
labels: Record<string, string>
|
|
34
|
+
registerLabel: (value: string, label: string) => void
|
|
35
|
+
listboxId: string
|
|
36
|
+
triggerRef: RefObject<unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SelectContext = createContext<NativeSelectContextValue | null>(null)
|
|
40
|
+
|
|
41
|
+
function useSelect() {
|
|
42
|
+
const ctx = useContext(SelectContext)
|
|
43
|
+
if (!ctx) throw new Error('Select compound components must be used within Select.Root')
|
|
44
|
+
return ctx
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True while Popup renders its items unseen, so each registers its label
|
|
49
|
+
* before the sheet first opens (the web reads them from the hidden list).
|
|
50
|
+
*/
|
|
51
|
+
const CollectContext = createContext(false)
|
|
52
|
+
|
|
53
|
+
interface RootProps {
|
|
54
|
+
/** The web's hidden form input; native has no form post. */
|
|
55
|
+
name?: string
|
|
56
|
+
defaultValue?: string
|
|
57
|
+
value?: string
|
|
58
|
+
onValueChange?: (value: string | null) => void
|
|
59
|
+
initialLabels?: Record<string, string>
|
|
60
|
+
children: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Native Select (plan N0, Slice C): the trigger opens a bottom sheet (a
|
|
65
|
+
* Drawer at the medium and large detents) listing the options; a press picks
|
|
66
|
+
* one and closes the sheet (C0 direction A, "Check"). The screen reader's
|
|
67
|
+
* focus returns to the trigger.
|
|
68
|
+
*/
|
|
69
|
+
function Root({defaultValue, value, onValueChange, initialLabels, children}: RootProps) {
|
|
70
|
+
const state = useSelectState({defaultValue, value, onValueChange, initialLabels})
|
|
71
|
+
const {open, setOpen, setValue} = state
|
|
72
|
+
const listboxId = useId()
|
|
73
|
+
const triggerRef = useRef<unknown>(null)
|
|
74
|
+
|
|
75
|
+
const wasOpen = useRef(open)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (wasOpen.current && !open) focusForAccessibility(triggerRef.current, 'Select')
|
|
78
|
+
wasOpen.current = open
|
|
79
|
+
}, [open])
|
|
80
|
+
|
|
81
|
+
const ctx: NativeSelectContextValue = {
|
|
82
|
+
value: state.value,
|
|
83
|
+
open,
|
|
84
|
+
setOpen,
|
|
85
|
+
choose: (v) => {
|
|
86
|
+
setValue(v)
|
|
87
|
+
setOpen(false)
|
|
88
|
+
},
|
|
89
|
+
labels: state.labels,
|
|
90
|
+
registerLabel: state.registerLabel,
|
|
91
|
+
listboxId,
|
|
92
|
+
triggerRef,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<SelectContext.Provider value={ctx}>
|
|
97
|
+
<Drawer.Root open={open} onOpenChange={setOpen} anchor="bottom" snapPoints={SNAP_POINTS}>
|
|
98
|
+
<html.div style={[styles.root, nativeStyles.nativeFlex]}>{children}</html.div>
|
|
99
|
+
</Drawer.Root>
|
|
100
|
+
</SelectContext.Provider>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* An RN Pressable host, so it carries an enclosing Tooltip's text as its
|
|
106
|
+
* accessibility hint (RSD drops `accessibilityHint` on html.*).
|
|
107
|
+
*/
|
|
108
|
+
function Trigger({
|
|
109
|
+
children,
|
|
110
|
+
'aria-label': ariaLabel,
|
|
111
|
+
size = 'default',
|
|
112
|
+
}: {
|
|
113
|
+
children: ReactNode
|
|
114
|
+
'aria-label'?: string
|
|
115
|
+
size?: ControlSize
|
|
116
|
+
}) {
|
|
117
|
+
const {open, setOpen, triggerRef} = useSelect()
|
|
118
|
+
const hint = useTooltipHint()
|
|
119
|
+
return (
|
|
120
|
+
<Pressable
|
|
121
|
+
ref={triggerRef as RefObject<never>}
|
|
122
|
+
onPress={() => setOpen(true)}
|
|
123
|
+
accessibilityRole="combobox"
|
|
124
|
+
accessibilityLabel={ariaLabel}
|
|
125
|
+
accessibilityHint={hint}
|
|
126
|
+
accessibilityState={{expanded: open}}
|
|
127
|
+
>
|
|
128
|
+
<html.div
|
|
129
|
+
style={[styles.trigger, nativeStyles.nativeFlex, size === 'small' && styles.triggerSmall]}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</html.div>
|
|
133
|
+
</Pressable>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Value({placeholder}: {placeholder?: string}) {
|
|
138
|
+
const {value, labels} = useSelect()
|
|
139
|
+
const display = value ? (labels[value] ?? value) : null
|
|
140
|
+
return (
|
|
141
|
+
<html.span style={display ? styles.value : styles.placeholder}>
|
|
142
|
+
{display ?? placeholder}
|
|
143
|
+
</html.span>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function Icon({children}: {children?: ReactNode}) {
|
|
148
|
+
return (
|
|
149
|
+
<html.div style={[styles.icon, nativeStyles.nativeFlex, nativeStyles.iconDown]}>
|
|
150
|
+
{children ?? <Chevron color="textMuted" />}
|
|
151
|
+
</html.div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface PopupProps {
|
|
156
|
+
children: ReactNode
|
|
157
|
+
/** The sheet's title (native) and the list's accessible name. */
|
|
158
|
+
label?: string
|
|
159
|
+
/** The sheet header's text button that closes it unchanged (native). Default: 'Cancel' */
|
|
160
|
+
cancelLabel?: string
|
|
161
|
+
/** Shown in place of the list when there are no items. */
|
|
162
|
+
empty?: ReactNode
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function Popup({children, label, cancelLabel = 'Cancel', empty}: PopupProps) {
|
|
166
|
+
const {open, choose, listboxId} = useSelect()
|
|
167
|
+
const hasItems = Children.toArray(children).length > 0
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<CollectContext.Provider value={true}>{children}</CollectContext.Provider>
|
|
171
|
+
<Drawer.Portal>
|
|
172
|
+
<Drawer.Header titleAlign="center">
|
|
173
|
+
{label ? <Drawer.Title>{label}</Drawer.Title> : null}
|
|
174
|
+
<Drawer.Close>
|
|
175
|
+
<Button variant="link">{cancelLabel}</Button>
|
|
176
|
+
</Drawer.Close>
|
|
177
|
+
</Drawer.Header>
|
|
178
|
+
{!hasItems && empty != null ? (
|
|
179
|
+
<Listbox.Empty>{empty}</Listbox.Empty>
|
|
180
|
+
) : (
|
|
181
|
+
<Drawer.Body flush>
|
|
182
|
+
<Listbox.Root id={listboxId} open={open} onSelect={choose} aria-label={label}>
|
|
183
|
+
{children}
|
|
184
|
+
</Listbox.Root>
|
|
185
|
+
</Drawer.Body>
|
|
186
|
+
)}
|
|
187
|
+
</Drawer.Portal>
|
|
188
|
+
</>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface ItemProps {
|
|
193
|
+
value: string
|
|
194
|
+
children: ReactNode
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function Item({value: itemValue, children}: ItemProps) {
|
|
198
|
+
const collecting = useContext(CollectContext)
|
|
199
|
+
const {value, registerLabel} = useSelect()
|
|
200
|
+
const label = textOf(children)
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (collecting && label) registerLabel(itemValue, label)
|
|
203
|
+
}, [collecting, itemValue, label, registerLabel])
|
|
204
|
+
if (collecting) return null
|
|
205
|
+
return (
|
|
206
|
+
<Listbox.Option value={itemValue} selected={value === itemValue}>
|
|
207
|
+
{children}
|
|
208
|
+
</Listbox.Option>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ItemText({children}: {children: ReactNode}) {
|
|
213
|
+
return <html.span>{children}</html.span>
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const Select = {
|
|
217
|
+
Root,
|
|
218
|
+
Trigger,
|
|
219
|
+
Value,
|
|
220
|
+
Icon,
|
|
221
|
+
Popup,
|
|
222
|
+
Item,
|
|
223
|
+
ItemText,
|
|
224
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
-
import {expect, screen} from 'storybook/test'
|
|
2
|
+
import {expect, fn, screen} from 'storybook/test'
|
|
3
3
|
import {Dialog} from '../Dialog/Dialog'
|
|
4
4
|
import {Select} from './Select'
|
|
5
5
|
|
|
@@ -213,3 +213,51 @@ export const ArrowKeysOpen: Story = {
|
|
|
213
213
|
await userEvent.keyboard('{Escape}')
|
|
214
214
|
},
|
|
215
215
|
}
|
|
216
|
+
|
|
217
|
+
const MODELS = ['claude-opus-5-5', 'claude-sonnet-5-5', 'claude-haiku-5-5', 'gpt-6', 'gemini-3-pro']
|
|
218
|
+
|
|
219
|
+
interface LabelledArgs {
|
|
220
|
+
label: string
|
|
221
|
+
empty: string
|
|
222
|
+
itemCount: number
|
|
223
|
+
onValueChange?: (value: string | null) => void
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Popup's label names the list (and titles the native sheet); empty replaces
|
|
228
|
+
* the list when there are no items. Set itemCount to 0 for the empty state.
|
|
229
|
+
*/
|
|
230
|
+
export const LabelledAndEmpty: StoryObj<LabelledArgs> = {
|
|
231
|
+
args: {
|
|
232
|
+
label: 'Model',
|
|
233
|
+
empty: 'No models. Connect a provider to choose a model.',
|
|
234
|
+
itemCount: 0,
|
|
235
|
+
onValueChange: fn(),
|
|
236
|
+
},
|
|
237
|
+
argTypes: {
|
|
238
|
+
label: {control: 'text', description: "Select.Popup: the list's accessible name"},
|
|
239
|
+
empty: {control: 'text', description: 'Select.Popup: shown when there are no items'},
|
|
240
|
+
itemCount: {control: {type: 'range', min: 0, max: MODELS.length, step: 1}},
|
|
241
|
+
},
|
|
242
|
+
render: ({label, empty, itemCount, onValueChange}) => (
|
|
243
|
+
<Select.Root onValueChange={onValueChange}>
|
|
244
|
+
<Select.Trigger aria-label={label}>
|
|
245
|
+
<Select.Value placeholder="Choose a model" />
|
|
246
|
+
<Select.Icon />
|
|
247
|
+
</Select.Trigger>
|
|
248
|
+
<Select.Popup label={label} empty={empty}>
|
|
249
|
+
{MODELS.slice(0, itemCount).map((m) => (
|
|
250
|
+
<Select.Item key={m} value={m}>
|
|
251
|
+
{m}
|
|
252
|
+
</Select.Item>
|
|
253
|
+
))}
|
|
254
|
+
</Select.Popup>
|
|
255
|
+
</Select.Root>
|
|
256
|
+
),
|
|
257
|
+
play: async ({canvas, userEvent}) => {
|
|
258
|
+
await userEvent.click(canvas.getByRole('combobox'))
|
|
259
|
+
const list = screen.getByRole('listbox', {name: 'Model'})
|
|
260
|
+
await expect(list).toHaveTextContent(/No models/)
|
|
261
|
+
await expect(screen.queryAllByRole('option')).toHaveLength(0)
|
|
262
|
+
},
|
|
263
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type ReactNode, useRef, useId, useEffect, useLayoutEffect, useState} from 'react'
|
|
1
|
+
import {Children, type ReactNode, useRef, useId, useEffect, useLayoutEffect, useState} from 'react'
|
|
2
2
|
import {createPortal} from 'react-dom'
|
|
3
3
|
import {html} from 'react-strict-dom'
|
|
4
4
|
import {styles} from './styles.css'
|
|
@@ -126,7 +126,17 @@ function Icon({children}: {children?: ReactNode}) {
|
|
|
126
126
|
// `overflow: hidden` / `transform` containing blocks (notably Dialog/Drawer).
|
|
127
127
|
// The listbox stays mounted while closed (display: none) so Items can register
|
|
128
128
|
// their labels from DOM text before the popup is ever opened.
|
|
129
|
-
|
|
129
|
+
interface PopupProps {
|
|
130
|
+
children: ReactNode
|
|
131
|
+
/** The list's accessible name; on native also the sheet's title. */
|
|
132
|
+
label?: string
|
|
133
|
+
/** Native only: the sheet header's text button that closes it unchanged. Default: 'Cancel' */
|
|
134
|
+
cancelLabel?: string
|
|
135
|
+
/** Shown in place of the list when there are no items. */
|
|
136
|
+
empty?: ReactNode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Popup({children, label, empty}: PopupProps) {
|
|
130
140
|
const {open, close, listboxId, triggerRef} = useSelect()
|
|
131
141
|
const mount = usePortalMount()
|
|
132
142
|
const [coords, setCoords] = useState<{top: number; left: number; width: number} | null>(null)
|
|
@@ -164,6 +174,7 @@ function Popup({children}: {children: ReactNode}) {
|
|
|
164
174
|
ref={listboxRef}
|
|
165
175
|
id={listboxId}
|
|
166
176
|
role="listbox"
|
|
177
|
+
aria-label={label}
|
|
167
178
|
aria-hidden={!open}
|
|
168
179
|
style={[
|
|
169
180
|
styles.popup,
|
|
@@ -171,7 +182,13 @@ function Popup({children}: {children: ReactNode}) {
|
|
|
171
182
|
coords && styles.popupPosition(coords.top, coords.left, coords.width),
|
|
172
183
|
]}
|
|
173
184
|
>
|
|
174
|
-
<ControlContextBoundary>
|
|
185
|
+
<ControlContextBoundary>
|
|
186
|
+
{empty != null && Children.toArray(children).length === 0 ? (
|
|
187
|
+
<html.div style={styles.empty}>{empty}</html.div>
|
|
188
|
+
) : (
|
|
189
|
+
children
|
|
190
|
+
)}
|
|
191
|
+
</ControlContextBoundary>
|
|
175
192
|
</html.div>
|
|
176
193
|
</>
|
|
177
194
|
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
|
|
3
|
+
/** Native Select additions to the shared styles. */
|
|
4
|
+
export const nativeStyles = css.create({
|
|
5
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
6
|
+
nativeFlex: {
|
|
7
|
+
display: 'flex',
|
|
8
|
+
},
|
|
9
|
+
// SideNav's chevron points right; a picker's points down.
|
|
10
|
+
iconDown: {
|
|
11
|
+
transform: 'rotate(90deg)',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {useCallback, useState} from 'react'
|
|
2
|
+
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
3
|
+
|
|
4
|
+
export interface SelectStateOptions {
|
|
5
|
+
defaultValue?: string
|
|
6
|
+
value?: string
|
|
7
|
+
onValueChange?: (value: string | null) => void
|
|
8
|
+
initialLabels?: Record<string, string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The Select's state both platforms share (plan N0, Decision 10): the value
|
|
13
|
+
* (controlled or not), whether it is open, and each option's label. Focus,
|
|
14
|
+
* highlight, document order and keys are the web adapter's (useSelectRoot);
|
|
15
|
+
* native presses and the sheet are Select.native's.
|
|
16
|
+
*/
|
|
17
|
+
export function useSelectState({
|
|
18
|
+
defaultValue,
|
|
19
|
+
value: controlledValue,
|
|
20
|
+
onValueChange,
|
|
21
|
+
initialLabels,
|
|
22
|
+
}: SelectStateOptions) {
|
|
23
|
+
const [value, setValue] = useControllableValue<string | null>(
|
|
24
|
+
controlledValue,
|
|
25
|
+
defaultValue ?? null,
|
|
26
|
+
onValueChange,
|
|
27
|
+
)
|
|
28
|
+
const [open, setOpen] = useState(false)
|
|
29
|
+
const [labels, setLabels] = useState<Record<string, string>>(initialLabels ?? {})
|
|
30
|
+
|
|
31
|
+
const registerLabel = useCallback((v: string, label: string) => {
|
|
32
|
+
setLabels((prev) => (prev[v] === label ? prev : {...prev, [v]: label}))
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
return {value, setValue, open, setOpen, labels, registerLabel}
|
|
36
|
+
}
|