@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.
Files changed (270) hide show
  1. package/README.md +49 -0
  2. package/dist/Table-DnaoH-h5.js +1806 -0
  3. package/dist/Table-DnaoH-h5.js.map +1 -0
  4. package/dist/components/AppShell/native.css.d.ts +53 -0
  5. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  6. package/dist/components/Button/Button.d.ts.map +1 -1
  7. package/dist/components/ColorMode/ColorMode.d.ts +2 -1
  8. package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
  9. package/dist/components/ColorMode/storage.d.ts +4 -0
  10. package/dist/components/ColorMode/storage.d.ts.map +1 -0
  11. package/dist/components/ColorMode/styles.css.d.ts +4 -0
  12. package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
  13. package/dist/components/ColorMode/useSystemScheme.d.ts +7 -0
  14. package/dist/components/ColorMode/useSystemScheme.d.ts.map +1 -0
  15. package/dist/components/Combobox/Combobox.d.ts +7 -2
  16. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  17. package/dist/components/Combobox/comboboxState.d.ts +29 -0
  18. package/dist/components/Combobox/comboboxState.d.ts.map +1 -0
  19. package/dist/components/Combobox/native.css.d.ts +27 -0
  20. package/dist/components/Combobox/native.css.d.ts.map +1 -0
  21. package/dist/components/Combobox/useComboboxRoot.d.ts +6 -10
  22. package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -1
  23. package/dist/components/Drawer/Drawer.d.ts +12 -37
  24. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  25. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  26. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  27. package/dist/components/Drawer/DrawerSnap.stories.d.ts +22 -0
  28. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  29. package/dist/components/Drawer/native.css.d.ts +59 -0
  30. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  31. package/dist/components/Drawer/parts.css.d.ts +96 -0
  32. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  33. package/dist/components/Drawer/parts.d.ts +56 -0
  34. package/dist/components/Drawer/parts.d.ts.map +1 -0
  35. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  36. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  37. package/dist/components/Drawer/styles.css.d.ts +16 -57
  38. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  39. package/dist/components/Drawer/touchInput.d.ts +35 -0
  40. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  41. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  42. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  43. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  44. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  45. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  46. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  47. package/dist/components/Form/FormSubmitContext.d.ts +8 -0
  48. package/dist/components/Form/FormSubmitContext.d.ts.map +1 -0
  49. package/dist/components/Listbox/native.css.d.ts +84 -0
  50. package/dist/components/Listbox/native.css.d.ts.map +1 -0
  51. package/dist/components/Menu/Menu.d.ts +7 -1
  52. package/dist/components/Menu/Menu.d.ts.map +1 -1
  53. package/dist/components/Menu/native.css.d.ts +14 -0
  54. package/dist/components/Menu/native.css.d.ts.map +1 -0
  55. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  56. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  57. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  58. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  59. package/dist/components/Popover/native.css.d.ts +11 -0
  60. package/dist/components/Popover/native.css.d.ts.map +1 -0
  61. package/dist/components/Select/Select.d.ts +9 -2
  62. package/dist/components/Select/Select.d.ts.map +1 -1
  63. package/dist/components/Select/Select.stories.d.ts +11 -0
  64. package/dist/components/Select/Select.stories.d.ts.map +1 -1
  65. package/dist/components/Select/native.css.d.ts +11 -0
  66. package/dist/components/Select/native.css.d.ts.map +1 -0
  67. package/dist/components/Select/selectState.d.ts +21 -0
  68. package/dist/components/Select/selectState.d.ts.map +1 -0
  69. package/dist/components/Select/styles.css.d.ts +10 -0
  70. package/dist/components/Select/styles.css.d.ts.map +1 -1
  71. package/dist/components/Select/useSelectRoot.d.ts +6 -8
  72. package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
  73. package/dist/components/SideNav/Chevron.d.ts +4 -0
  74. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  75. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  76. package/dist/components/SideNav/styles.css.d.ts +6 -0
  77. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  78. package/dist/components/SideNav/types.d.ts +6 -0
  79. package/dist/components/SideNav/types.d.ts.map +1 -0
  80. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  81. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  82. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  83. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  84. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  85. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  86. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  87. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  88. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  89. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  90. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  91. package/dist/components/Tabs/indicator.d.ts +28 -0
  92. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  93. package/dist/components/Tabs/styles.css.d.ts +4 -0
  94. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  95. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  96. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  97. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  98. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  99. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  100. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  101. package/dist/components/Tag/Tag.d.ts +2 -24
  102. package/dist/components/Tag/Tag.d.ts.map +1 -1
  103. package/dist/components/Tag/styles.css.d.ts +8 -0
  104. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  105. package/dist/components/Tag/types.d.ts +39 -0
  106. package/dist/components/Tag/types.d.ts.map +1 -0
  107. package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts +13 -0
  108. package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts.map +1 -0
  109. package/dist/components/ThemeProvider/ThemeProvider.d.ts +9 -2
  110. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  111. package/dist/components/ThemeProvider/overrideRule.d.ts +7 -0
  112. package/dist/components/ThemeProvider/overrideRule.d.ts.map +1 -0
  113. package/dist/components/ThemeProvider/overrides.d.ts +16 -0
  114. package/dist/components/ThemeProvider/overrides.d.ts.map +1 -0
  115. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  116. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  117. package/dist/components/Toast/native.css.d.ts +81 -0
  118. package/dist/components/Toast/native.css.d.ts.map +1 -0
  119. package/dist/components/Toast/store.d.ts +47 -0
  120. package/dist/components/Toast/store.d.ts.map +1 -0
  121. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  122. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  123. package/dist/components/Tree/Tree.d.ts.map +1 -1
  124. package/dist/components/Tree/styles.css.d.ts +6 -0
  125. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  126. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  127. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  128. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  129. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  130. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  131. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  132. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  133. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  134. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  135. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  136. package/dist/index.css +1 -1
  137. package/dist/index.d.ts +4 -2
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +5090 -4381
  140. package/dist/index.js.map +1 -1
  141. package/dist/shared/ButtonHost.d.ts +11 -0
  142. package/dist/shared/ButtonHost.d.ts.map +1 -0
  143. package/dist/shared/pressableChild.d.ts +14 -0
  144. package/dist/shared/pressableChild.d.ts.map +1 -0
  145. package/dist/shared/textOf.d.ts +7 -0
  146. package/dist/shared/textOf.d.ts.map +1 -0
  147. package/dist/table.js +1 -1
  148. package/package.json +3 -2
  149. package/src/components/AppShell/AppShell.meta.ts +1 -1
  150. package/src/components/AppShell/AppShell.native.tsx +174 -0
  151. package/src/components/AppShell/native.css.ts +56 -0
  152. package/src/components/Button/Button.tsx +15 -4
  153. package/src/components/ColorMode/ColorMode.meta.ts +1 -1
  154. package/src/components/ColorMode/ColorMode.tsx +16 -37
  155. package/src/components/ColorMode/storage.native.ts +8 -0
  156. package/src/components/ColorMode/storage.ts +19 -0
  157. package/src/components/ColorMode/styles.css.ts +6 -0
  158. package/src/components/ColorMode/useSystemScheme.native.ts +6 -0
  159. package/src/components/ColorMode/useSystemScheme.ts +19 -0
  160. package/src/components/Combobox/Combobox.meta.ts +1 -1
  161. package/src/components/Combobox/Combobox.native.tsx +295 -0
  162. package/src/components/Combobox/Combobox.tsx +14 -8
  163. package/src/components/Combobox/comboboxState.ts +107 -0
  164. package/src/components/Combobox/native.css.ts +33 -0
  165. package/src/components/Combobox/useComboboxRoot.ts +17 -91
  166. package/src/components/Drawer/Drawer.meta.ts +2 -1
  167. package/src/components/Drawer/Drawer.native.tsx +491 -0
  168. package/src/components/Drawer/Drawer.tsx +65 -143
  169. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  170. package/src/components/Drawer/DrawerSnap.stories.tsx +225 -0
  171. package/src/components/Drawer/native.css.ts +62 -0
  172. package/src/components/Drawer/parts.css.ts +125 -0
  173. package/src/components/Drawer/parts.tsx +110 -0
  174. package/src/components/Drawer/sheetPhysics.ts +103 -0
  175. package/src/components/Drawer/styles.css.ts +18 -73
  176. package/src/components/Drawer/touchInput.ts +81 -0
  177. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  178. package/src/components/Drawer/useSheetHeights.ts +42 -0
  179. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  180. package/src/components/Form/Form.meta.ts +1 -1
  181. package/src/components/Form/Form.native.tsx +120 -0
  182. package/src/components/Form/FormSubmitContext.ts +12 -0
  183. package/src/components/Listbox/Listbox.meta.ts +1 -1
  184. package/src/components/Listbox/Listbox.native.tsx +153 -0
  185. package/src/components/Listbox/native.css.ts +91 -0
  186. package/src/components/Menu/Menu.meta.ts +1 -1
  187. package/src/components/Menu/Menu.native.tsx +268 -0
  188. package/src/components/Menu/Menu.tsx +11 -2
  189. package/src/components/Menu/native.css.ts +18 -0
  190. package/src/components/Menu/useMenuOpen.ts +33 -0
  191. package/src/components/Menu/useMenuRoot.ts +7 -26
  192. package/src/components/Popover/Popover.meta.ts +1 -1
  193. package/src/components/Popover/Popover.native.tsx +205 -0
  194. package/src/components/Popover/native.css.ts +13 -0
  195. package/src/components/ScrollArea/ScrollArea.meta.ts +1 -1
  196. package/src/components/ScrollArea/ScrollArea.native.tsx +61 -0
  197. package/src/components/Select/Select.meta.ts +1 -1
  198. package/src/components/Select/Select.native.tsx +224 -0
  199. package/src/components/Select/Select.stories.tsx +49 -1
  200. package/src/components/Select/Select.tsx +20 -3
  201. package/src/components/Select/native.css.ts +13 -0
  202. package/src/components/Select/selectState.ts +36 -0
  203. package/src/components/Select/styles.css.ts +11 -0
  204. package/src/components/Select/useSelectRoot.ts +9 -30
  205. package/src/components/SideNav/Chevron.native.tsx +20 -0
  206. package/src/components/SideNav/Chevron.tsx +20 -0
  207. package/src/components/SideNav/SideNav.meta.ts +1 -1
  208. package/src/components/SideNav/SideNav.tsx +57 -41
  209. package/src/components/SideNav/styles.css.ts +8 -0
  210. package/src/components/SideNav/types.ts +6 -0
  211. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  212. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  213. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  214. package/src/components/SplitPane/SplitPane.tsx +218 -0
  215. package/src/components/SplitPane/styles.css.ts +81 -0
  216. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  217. package/src/components/Tabs/TabLayout.tsx +6 -0
  218. package/src/components/Tabs/Tabs.meta.ts +1 -1
  219. package/src/components/Tabs/Tabs.tsx +38 -136
  220. package/src/components/Tabs/indicator.ts +36 -0
  221. package/src/components/Tabs/styles.css.ts +7 -0
  222. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  223. package/src/components/Tabs/useTabIndicator.ts +49 -0
  224. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  225. package/src/components/Tabs/useTabKeys.ts +99 -0
  226. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  227. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  228. package/src/components/Tag/Tag.meta.ts +1 -1
  229. package/src/components/Tag/Tag.native.tsx +70 -0
  230. package/src/components/Tag/Tag.tsx +9 -37
  231. package/src/components/Tag/styles.css.ts +9 -0
  232. package/src/components/Tag/types.ts +43 -0
  233. package/src/components/ThemeProvider/ThemeOverrides.stories.tsx +113 -0
  234. package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -1
  235. package/src/components/ThemeProvider/ThemeProvider.native.tsx +36 -7
  236. package/src/components/ThemeProvider/ThemeProvider.tsx +42 -6
  237. package/src/components/ThemeProvider/overrideRule.ts +39 -0
  238. package/src/components/ThemeProvider/overrides.ts +27 -0
  239. package/src/components/Toast/Toast.meta.ts +1 -1
  240. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  241. package/src/components/Toast/ToastProvider.tsx +22 -84
  242. package/src/components/Toast/native.css.ts +78 -0
  243. package/src/components/Toast/store.ts +96 -0
  244. package/src/components/Toggle/Toggle.tsx +3 -3
  245. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  246. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  247. package/src/components/Tooltip/TooltipHint.ts +13 -0
  248. package/src/components/Tree/Tree.meta.ts +1 -1
  249. package/src/components/Tree/Tree.tsx +23 -135
  250. package/src/components/Tree/styles.css.ts +6 -0
  251. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  252. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  253. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  254. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  255. package/src/components/VirtualList/VirtualList.tsx +103 -0
  256. package/src/components/VirtualList/styles.css.ts +31 -0
  257. package/src/form.native.ts +7 -0
  258. package/src/hooks/useContainerBelow.native.ts +30 -0
  259. package/src/index.native.ts +48 -0
  260. package/src/index.ts +9 -1
  261. package/src/shared/ButtonHost.native.tsx +70 -0
  262. package/src/shared/ButtonHost.tsx +12 -0
  263. package/src/shared/focusInput.native.ts +18 -0
  264. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  265. package/src/shared/pressableChild.ts +33 -0
  266. package/src/shared/textOf.ts +13 -0
  267. package/src/shared/useKeyboardHeight.native.ts +18 -0
  268. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  269. package/dist/Table-ColfpgKV.js +0 -1552
  270. 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
- function Popup({children}: {children: ReactNode}) {
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>{children}</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
+ }