@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
@@ -1,28 +1,29 @@
1
1
  import {createPortal} from 'react-dom'
2
- import {
3
- type ReactNode,
4
- type RefObject,
5
- createContext,
6
- useContext,
7
- useState,
8
- useEffect,
9
- useCallback,
10
- useId,
11
- useRef,
12
- } from 'react'
2
+ import {type ReactNode, useState, useEffect, useCallback} from 'react'
13
3
  import {html} from 'react-strict-dom'
14
4
  import type {DurationToken} from '@duro-app/tokens/keys'
15
5
  import {styles} from './styles.css'
16
- import {useOverlayOpenState} from '../../shared/useOverlayOpenState'
17
6
  import {usePortalMount} from '../ThemeProvider/ThemeProvider'
18
7
  import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
19
8
  import {ModalContext} from '../../shared/ModalContext'
20
9
  import {useSwipeDismiss} from './useSwipeDismiss'
10
+ import {useSheetHeights} from './useSheetHeights'
11
+ import {cycleDetent} from './sheetPhysics'
12
+ import {partStyles} from './parts.css'
13
+ import {
14
+ DEFAULT_GRABBER_LABELS,
15
+ Description,
16
+ DrawerContext,
17
+ Footer,
18
+ Header,
19
+ Title,
20
+ useDrawer,
21
+ type DrawerSize,
22
+ type GrabberLabels,
23
+ } from './parts'
24
+ import {useDrawerRoot, type DrawerRootProps} from './useDrawerRoot'
21
25
 
22
- // --- Types ---
23
-
24
- export type DrawerAnchor = 'right' | 'left' | 'bottom'
25
- export type DrawerSize = 'sm' | 'md' | 'lg'
26
+ export type {DrawerAnchor, DrawerSize, SnapPoint} from './parts'
26
27
 
27
28
  const closeDurationMap = {
28
29
  instant: styles.closeDurationInstant,
@@ -34,79 +35,11 @@ const closeDurationMap = {
34
35
  slow: styles.closeDurationSlow,
35
36
  } as const satisfies Record<DurationToken, unknown>
36
37
 
37
- // --- Context ---
38
-
39
- interface DrawerContextValue {
40
- open: boolean
41
- closing: boolean
42
- closeDuration: DurationToken
43
- anchor: DrawerAnchor
44
- dismissable: boolean
45
- swipeDismiss: boolean
46
- requestOpen: () => void
47
- requestClose: () => void
48
- /** Close immediately without CSS animation (used by swipe dismiss which handles its own exit animation) */
49
- requestCloseImmediate: () => void
50
- titleId: string
51
- descriptionId: string
52
- panelRef: RefObject<HTMLDivElement | null>
53
- }
54
-
55
- const DrawerContext = createContext<DrawerContextValue | null>(null)
56
-
57
- function useDrawer() {
58
- const ctx = useContext(DrawerContext)
59
- if (!ctx) throw new Error('Drawer compound components must be used within Drawer.Root')
60
- return ctx
61
- }
62
-
63
38
  // --- Root ---
64
39
 
65
- interface RootProps {
66
- children: ReactNode
67
- /** Controlled open state */
68
- open?: boolean
69
- /** Default open state (uncontrolled) */
70
- defaultOpen?: boolean
71
- /** Called when drawer open state changes */
72
- onOpenChange?: (open: boolean) => void
73
- /** Which edge the drawer slides from. Default: 'right' */
74
- anchor?: DrawerAnchor
75
- /** Whether clicking backdrop or pressing Escape closes the drawer. Default: true */
76
- dismissable?: boolean
77
- /** Whether swipe gesture dismisses the drawer. Default: true */
78
- swipeDismiss?: boolean
79
- /** Motion token for the close animation. Default: 'fast' (150ms) */
80
- closeAnimationDuration?: DurationToken
81
- }
82
-
83
- function Root({
84
- children,
85
- open: controlledOpen,
86
- defaultOpen = false,
87
- onOpenChange,
88
- anchor = 'right',
89
- dismissable = true,
90
- swipeDismiss = true,
91
- closeAnimationDuration = 'fast',
92
- }: RootProps) {
93
- const {
94
- present,
95
- closing,
96
- requestOpen,
97
- requestClose,
98
- closeNow: requestCloseImmediate,
99
- } = useOverlayOpenState({
100
- open: controlledOpen,
101
- defaultOpen,
102
- onOpenChange,
103
- exitDuration: closeAnimationDuration,
104
- closeNotice: 'after-exit',
105
- })
106
- const panelRef = useRef<HTMLDivElement>(null)
107
-
108
- const titleId = useId()
109
- const descriptionId = useId()
40
+ function Root({children, ...props}: DrawerRootProps) {
41
+ const ctx = useDrawerRoot(props)
42
+ const {open: present, dismissable, requestClose} = ctx
110
43
 
111
44
  // Handle Escape key
112
45
  useEffect(() => {
@@ -134,26 +67,7 @@ function Root({
134
67
  }
135
68
  }, [present])
136
69
 
137
- return (
138
- <DrawerContext.Provider
139
- value={{
140
- open: present,
141
- closing,
142
- closeDuration: closeAnimationDuration,
143
- anchor,
144
- dismissable,
145
- swipeDismiss,
146
- requestOpen,
147
- requestClose,
148
- requestCloseImmediate,
149
- titleId,
150
- descriptionId,
151
- panelRef,
152
- }}
153
- >
154
- {children}
155
- </DrawerContext.Provider>
156
- )
70
+ return <DrawerContext.Provider value={ctx}>{children}</DrawerContext.Provider>
157
71
  }
158
72
 
159
73
  // --- Trigger ---
@@ -162,7 +76,7 @@ function Trigger({children}: {children: ReactNode}) {
162
76
  const {requestOpen} = useDrawer()
163
77
 
164
78
  return (
165
- <html.div onClick={requestOpen} style={styles.inlineWrapper}>
79
+ <html.div onClick={requestOpen} style={partStyles.inlineWrapper}>
166
80
  {children}
167
81
  </html.div>
168
82
  )
@@ -173,6 +87,8 @@ function Trigger({children}: {children: ReactNode}) {
173
87
  interface PortalProps {
174
88
  children: ReactNode
175
89
  size?: DrawerSize
90
+ /** The grabber's accessible names, by what a press does next. Snap sheets only */
91
+ grabberLabels?: GrabberLabels
176
92
  }
177
93
 
178
94
  // Slide animation maps
@@ -194,7 +110,7 @@ const viewportAnchorMap = {
194
110
  bottom: styles.viewportBottom,
195
111
  } as const
196
112
 
197
- function Portal({children, size = 'md'}: PortalProps) {
113
+ function Portal({children, size = 'md', grabberLabels = DEFAULT_GRABBER_LABELS}: PortalProps) {
198
114
  const {
199
115
  open,
200
116
  closing,
@@ -207,8 +123,14 @@ function Portal({children, size = 'md'}: PortalProps) {
207
123
  titleId,
208
124
  descriptionId,
209
125
  panelRef,
126
+ snapPoints,
127
+ snapIndex,
128
+ setSnapIndex,
210
129
  } = useDrawer()
211
130
  const mount = usePortalMount()
131
+ const snapping = snapPoints.length > 0
132
+ const heights = useSheetHeights(snapPoints, snapping && open)
133
+ const sized = snapping && heights.length === snapPoints.length
212
134
  // The shown panel element, tracked as state so swipe listeners follow it
213
135
  // when the portal mount arrives and the panel is remounted into it.
214
136
  const [panel, setPanel] = useState<HTMLDivElement | null>(null)
@@ -232,6 +154,7 @@ function Portal({children, size = 'md'}: PortalProps) {
232
154
  enabled: swipeEnabled && open && !closing,
233
155
  onDismiss: requestCloseImmediate,
234
156
  panel,
157
+ snap: sized ? {heights, index: snapIndex, onSnap: setSnapIndex} : null,
235
158
  })
236
159
 
237
160
  if (!open) return null
@@ -265,12 +188,29 @@ function Portal({children, size = 'md'}: PortalProps) {
265
188
  isHorizontal && styles.panelHorizontal,
266
189
  isHorizontal && styles[size],
267
190
  !isHorizontal && styles.panelVertical,
191
+ sized && styles.panelSnap,
192
+ sized && styles.sheetHeight(heights[snapIndex]),
268
193
  anchor === 'left' && styles.panelLeftBorder,
269
194
  closing && slideOutMap[anchor],
270
195
  closing && closeDurationMap[closeDuration],
271
196
  !closing && slideInMap[anchor],
272
197
  ]}
273
198
  >
199
+ {snapping && (
200
+ <html.button
201
+ type="button"
202
+ data-drawer-grabber=""
203
+ aria-label={
204
+ cycleDetent(snapIndex, snapPoints.length) > snapIndex
205
+ ? grabberLabels.expand
206
+ : grabberLabels.collapse
207
+ }
208
+ onClick={() => setSnapIndex(cycleDetent(snapIndex, snapPoints.length))}
209
+ style={[partStyles.grabberHit, styles.grabberHitWeb]}
210
+ >
211
+ <html.span style={partStyles.grabber} />
212
+ </html.button>
213
+ )}
274
214
  <ModalContext.Provider value={true}>
275
215
  <ControlContextBoundary>{children}</ControlContextBoundary>
276
216
  </ModalContext.Provider>
@@ -287,46 +227,28 @@ function Portal({children, size = 'md'}: PortalProps) {
287
227
  return mount ? createPortal(node, mount) : node
288
228
  }
289
229
 
290
- // --- Header ---
230
+ // --- Body ---
291
231
 
292
- function Header({children}: {children: ReactNode}) {
293
- return <html.div style={styles.header}>{children}</html.div>
232
+ interface BodyProps {
233
+ children: ReactNode
234
+ /** No side padding: rows that run to the panel's edges, as a picker's list. */
235
+ flush?: boolean
294
236
  }
295
237
 
296
- // --- Title ---
297
-
298
- function Title({children}: {children: ReactNode}) {
299
- const {titleId} = useDrawer()
238
+ function Body({children, flush = false}: BodyProps) {
239
+ const {snapPoints} = useDrawer()
240
+ // A snap sheet's Body is its scroll viewport: a drag there scrolls it, and
241
+ // moves the sheet only from the top edge (or upward, below the largest detent).
300
242
  return (
301
- <html.h2 id={titleId} style={styles.title}>
302
- {children}
303
- </html.h2>
304
- )
305
- }
306
-
307
- // --- Description ---
308
-
309
- function Description({children}: {children: ReactNode}) {
310
- const {descriptionId} = useDrawer()
311
- return (
312
- <html.p id={descriptionId} style={styles.description}>
243
+ <html.div
244
+ style={[styles.body, flush && styles.bodyFlush]}
245
+ data-duro-scroll={snapPoints.length > 0 ? '' : undefined}
246
+ >
313
247
  {children}
314
- </html.p>
248
+ </html.div>
315
249
  )
316
250
  }
317
251
 
318
- // --- Body ---
319
-
320
- function Body({children}: {children: ReactNode}) {
321
- return <html.div style={styles.body}>{children}</html.div>
322
- }
323
-
324
- // --- Footer ---
325
-
326
- function Footer({children}: {children: ReactNode}) {
327
- return <html.div style={styles.footer}>{children}</html.div>
328
- }
329
-
330
252
  // --- Close ---
331
253
 
332
254
  interface CloseProps {
@@ -339,14 +261,14 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
339
261
 
340
262
  if (children) {
341
263
  return (
342
- <html.div onClick={requestClose} style={styles.inlineWrapper}>
264
+ <html.div onClick={requestClose} style={partStyles.inlineWrapper}>
343
265
  {children}
344
266
  </html.div>
345
267
  )
346
268
  }
347
269
 
348
270
  return (
349
- <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
271
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={partStyles.closeButton}>
350
272
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
351
273
  <path
352
274
  d="M4 4l8 8M12 4l-8 8"
@@ -1,11 +1,11 @@
1
1
  import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {expect, waitFor} from 'storybook/test'
3
- import {cdp, page} from 'vitest/browser'
4
3
  import {Drawer} from './Drawer'
5
4
  import {ToggleGroup} from '../ToggleGroup/ToggleGroup'
6
5
  import {Toggle} from '../Toggle/Toggle'
7
6
  import {Icon, type IconName} from '../Icon'
8
7
  import {SIZES_PX} from '@duro-app/tokens/keys'
8
+ import {cdpSend, pause, setViewport, toPage, touchDrag, type Point} from './touchInput'
9
9
 
10
10
  const meta: Meta = {
11
11
  title: 'Components/Drawer/Scroll',
@@ -87,56 +87,6 @@ function Scene({pressed}: {pressed?: string} = {}) {
87
87
  )
88
88
  }
89
89
 
90
- type Point = {x: number; y: number}
91
-
92
- /** `CDPSession` is empty until the provider's typings load; `send` exists at runtime. */
93
- const cdpSend = (method: string, params: object) =>
94
- (cdp() as unknown as {send: (method: string, params: object) => Promise<unknown>}).send(
95
- method,
96
- params,
97
- )
98
-
99
- const pause = (ms: number) => new Promise((r) => setTimeout(r, ms))
100
-
101
- /**
102
- * CDP input lands in top-level page coordinates; the story runs in an iframe
103
- * the test runner offsets and may scale to fit. Map a point up every frame.
104
- */
105
- function toPage(p: Point): Point {
106
- let {x, y} = p
107
- let win: Window = window
108
- while (win.frameElement && win.parent !== win) {
109
- const rect = win.frameElement.getBoundingClientRect()
110
- const scale = rect.width / win.innerWidth
111
- x = rect.left + x * scale
112
- y = rect.top + y * scale
113
- win = win.parent
114
- }
115
- return {x, y}
116
- }
117
-
118
- /**
119
- * A page-coordinate touch point with a 1px radius: Chrome's touch adjustment
120
- * snaps a fat default touch onto the nearest control, which would turn every
121
- * drag in the list into a drag on a toggle.
122
- */
123
- const touchAt = (p: Point) => ({...toPage(p), radiusX: 1, radiusY: 1})
124
-
125
- /** Real touch drag through CDP; synthetic pointer events don't scroll. */
126
- async function touchDrag(from: Point, to: Point) {
127
- const steps = 12
128
- const send = (type: string, touchPoints: Point[]) =>
129
- cdpSend('Input.dispatchTouchEvent', {type, touchPoints: touchPoints.map(touchAt)})
130
- await send('touchStart', [from])
131
- for (let i = 1; i <= steps; i++) {
132
- await send('touchMove', [
133
- {x: from.x + ((to.x - from.x) * i) / steps, y: from.y + ((to.y - from.y) * i) / steps},
134
- ])
135
- await pause(16)
136
- }
137
- await send('touchEnd', [])
138
- }
139
-
140
90
  const getViewport = () => document.querySelector<HTMLElement>('[data-duro-scroll]')!
141
91
  const getDialog = () => document.querySelector('[role="dialog"]')
142
92
 
@@ -158,7 +108,7 @@ function freePoint(el: HTMLElement, fy: number): Point {
158
108
  }
159
109
 
160
110
  async function setup() {
161
- await page.viewport(390, 844)
111
+ await setViewport(390, 844)
162
112
  await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
163
113
  await waitFor(() => expect(getViewport()).toBeTruthy())
164
114
  const viewport = getViewport()
@@ -242,7 +192,7 @@ export const TapOnToggleStillPresses: Story = {
242
192
  export const MountKeepsTheDrawerStill: Story = {
243
193
  // size the page before the render: a drawer mounts at its final width
244
194
  beforeEach: async () => {
245
- await page.viewport(390, 844)
195
+ await setViewport(390, 844)
246
196
  },
247
197
  render: () => <Scene pressed={ICON_NAMES[39]} />,
248
198
  play: async () => {
@@ -278,7 +228,7 @@ export const CoarsePointerRows: Story = {
278
228
  ),
279
229
  play: async () => {
280
230
  // phone width, so 42 icons overflow three rows
281
- await page.viewport(390, 844)
231
+ await setViewport(390, 844)
282
232
  await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
283
233
  await waitFor(() => expect(matchMedia('(pointer: coarse)').matches).toBe(true))
284
234
  const viewport = getViewport()
@@ -0,0 +1,225 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn, userEvent, waitFor} from 'storybook/test'
3
+ import {Drawer, type SnapPoint} from './Drawer'
4
+ import {Button} from '../Button/Button'
5
+ import {List} from '../List/List'
6
+ import {cdpSend, centre, pause, setViewport, touchDrag, type Point} from './touchInput'
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/Drawer/Snap points',
10
+ parameters: {layout: 'fullscreen'},
11
+ }
12
+
13
+ export default meta
14
+ type Story = StoryObj
15
+
16
+ const SESSIONS = Array.from({length: 40}, (_, i) => `Session ${i + 1}`)
17
+
18
+ interface SheetArgs {
19
+ snapPoints: SnapPoint[]
20
+ defaultSnapPoint: SnapPoint
21
+ titleAlign: 'start' | 'center'
22
+ flush: boolean
23
+ onOpenChange?: (open: boolean) => void
24
+ }
25
+
26
+ const DEFAULT_ARGS: SheetArgs = {
27
+ snapPoints: ['medium', 'large'],
28
+ defaultSnapPoint: 'medium',
29
+ titleAlign: 'center',
30
+ flush: false,
31
+ }
32
+
33
+ /** The picked B0 direction (docs/mockups/native-sheet/Done.dc.html): grabber, centred title, a text Done. */
34
+ function Sheet(props: Partial<SheetArgs>) {
35
+ const {snapPoints, defaultSnapPoint, titleAlign, flush, onOpenChange} = {
36
+ ...DEFAULT_ARGS,
37
+ ...props,
38
+ }
39
+ return (
40
+ <Drawer.Root
41
+ anchor="bottom"
42
+ snapPoints={snapPoints}
43
+ defaultSnapPoint={defaultSnapPoint}
44
+ onOpenChange={onOpenChange}
45
+ defaultOpen
46
+ >
47
+ <Drawer.Portal>
48
+ <Drawer.Header titleAlign={titleAlign}>
49
+ <Drawer.Title>Move to project</Drawer.Title>
50
+ <Drawer.Close>
51
+ <Button variant="link">Done</Button>
52
+ </Drawer.Close>
53
+ </Drawer.Header>
54
+ <Drawer.Body flush={flush}>
55
+ <List.Root aria-label="Sessions">
56
+ {SESSIONS.map((s) => (
57
+ <List.Item key={s}>
58
+ <List.Text>{s}</List.Text>
59
+ </List.Item>
60
+ ))}
61
+ </List.Root>
62
+ </Drawer.Body>
63
+ </Drawer.Portal>
64
+ </Drawer.Root>
65
+ )
66
+ }
67
+
68
+ export const SnapSheet: StoryObj<SheetArgs> = {
69
+ args: {...DEFAULT_ARGS, onOpenChange: fn()},
70
+ argTypes: {
71
+ snapPoints: {
72
+ control: 'check',
73
+ options: ['medium', 'large'],
74
+ description: "The detents: 'medium' half the window, 'large' the window less its top inset",
75
+ },
76
+ defaultSnapPoint: {
77
+ control: 'inline-radio',
78
+ options: ['medium', 'large'],
79
+ description: 'The detent the sheet opens at',
80
+ },
81
+ titleAlign: {
82
+ control: 'inline-radio',
83
+ options: ['start', 'center'],
84
+ description: 'Drawer.Header: the title at the start, or centred with the close at the end',
85
+ },
86
+ flush: {
87
+ control: 'boolean',
88
+ description:
89
+ "Drawer.Body: no side padding, so rows run to the panel's edges (a picker's list)",
90
+ },
91
+ },
92
+ // Keyed by the opening detent: it seeds state on mount, so a change remounts the sheet.
93
+ render: (args) => (
94
+ <Sheet
95
+ key={args.defaultSnapPoint}
96
+ {...args}
97
+ snapPoints={args.snapPoints.length > 0 ? args.snapPoints : ['medium']}
98
+ />
99
+ ),
100
+ }
101
+
102
+ const getDialog = () => document.querySelector<HTMLElement>('[role="dialog"]')
103
+ const getGrabber = () => document.querySelector<HTMLElement>('[data-drawer-grabber]')!
104
+ const getBody = () => document.querySelector<HTMLElement>('[role="dialog"] [data-duro-scroll]')!
105
+ const sheetHeight = () => getDialog()!.getBoundingClientRect().height
106
+
107
+ const WINDOW_H = 844
108
+ const MEDIUM = WINDOW_H / 2
109
+ const LARGE = WINDOW_H // no top safe-area inset in a desktop browser
110
+
111
+ async function setup() {
112
+ await setViewport(390, WINDOW_H)
113
+ await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
114
+ await waitFor(() => expect(getDialog()).not.toBeNull())
115
+ // let the slide-in finish
116
+ await pause(500)
117
+ }
118
+
119
+ const near = (actual: number, expected: number) =>
120
+ expect(Math.abs(actual - expected)).toBeLessThanOrEqual(2)
121
+
122
+ async function dragGrabber(dy: number) {
123
+ const from: Point = centre(getGrabber())
124
+ await touchDrag(from, {x: from.x, y: from.y + dy})
125
+ await pause(400)
126
+ }
127
+
128
+ // One drag per story: a test that drags three times can outlast the runner's
129
+ // timeout on a loaded machine.
130
+ export const DragUpGrowsToLarge: Story = {
131
+ render: () => <Sheet />,
132
+ play: async () => {
133
+ await setup()
134
+ await waitFor(() => near(sheetHeight(), MEDIUM))
135
+ await dragGrabber(-300)
136
+ await waitFor(() => near(sheetHeight(), LARGE))
137
+ },
138
+ }
139
+
140
+ export const DragDownShrinksToMedium: Story = {
141
+ render: () => <Sheet defaultSnapPoint="large" />,
142
+ play: async () => {
143
+ await setup()
144
+ await waitFor(() => near(sheetHeight(), LARGE))
145
+ await dragGrabber(300)
146
+ await waitFor(() => near(sheetHeight(), MEDIUM))
147
+ },
148
+ }
149
+
150
+ export const DragDownBelowMediumDismisses: Story = {
151
+ render: () => <Sheet />,
152
+ play: async () => {
153
+ await setup()
154
+ await waitFor(() => near(sheetHeight(), MEDIUM))
155
+ await dragGrabber(300)
156
+ await waitFor(() => expect(getDialog()).toBeNull())
157
+ },
158
+ }
159
+
160
+ export const SlowShortDragSnapsBack: Story = {
161
+ render: () => <Sheet />,
162
+ play: async () => {
163
+ await setup()
164
+ const from = centre(getGrabber())
165
+ // 60px over 20 moves (about 0.2 px/ms): well under the fling speed, well
166
+ // short of the next detent
167
+ await touchDrag(from, {x: from.x, y: from.y - 60}, 20)
168
+ await pause(400)
169
+ await waitFor(() => near(sheetHeight(), MEDIUM))
170
+ },
171
+ }
172
+
173
+ export const GrabberTapCyclesDetents: Story = {
174
+ render: () => <Sheet />,
175
+ play: async () => {
176
+ await setup()
177
+ const grabber = getGrabber()
178
+ await expect(grabber).toHaveAccessibleName('Expand sheet')
179
+ await userEvent.click(grabber)
180
+ await waitFor(() => near(sheetHeight(), LARGE))
181
+ await expect(grabber).toHaveAccessibleName('Shrink sheet')
182
+ await userEvent.click(grabber)
183
+ await waitFor(() => near(sheetHeight(), MEDIUM))
184
+ },
185
+ }
186
+
187
+ export const DragUpOnContentGrowsThenScrolls: Story = {
188
+ render: () => <Sheet />,
189
+ play: async () => {
190
+ await setup()
191
+ const from = centre(getBody())
192
+ // At medium, the content's upward drag grows the sheet …
193
+ await touchDrag(from, {x: from.x, y: from.y - 250})
194
+ await pause(400)
195
+ await waitFor(() => near(sheetHeight(), LARGE))
196
+ await expect(getBody().scrollTop).toBe(0)
197
+ // … at the largest detent it scrolls the list.
198
+ const again = centre(getBody())
199
+ await touchDrag(again, {x: again.x, y: again.y - 120})
200
+ await pause(300)
201
+ await expect(getBody().scrollTop).toBeGreaterThan(0)
202
+ near(sheetHeight(), LARGE)
203
+ },
204
+ }
205
+
206
+ export const DoneCloses: Story = {
207
+ render: () => <Sheet />,
208
+ play: async () => {
209
+ await setup()
210
+ await userEvent.click(
211
+ document.querySelector('[role="dialog"] button:not([data-drawer-grabber])')!,
212
+ )
213
+ await waitFor(() => expect(getDialog()).toBeNull())
214
+ },
215
+ }
216
+
217
+ export const FlushBody: Story = {
218
+ render: () => <Sheet flush />,
219
+ play: async () => {
220
+ await waitFor(() => expect(getDialog()).not.toBeNull())
221
+ const body = getBody()
222
+ await expect(getComputedStyle(body).paddingLeft).toBe('0px')
223
+ await expect(getComputedStyle(body).paddingRight).toBe('0px')
224
+ },
225
+ }
@@ -0,0 +1,62 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
5
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
6
+
7
+ /**
8
+ * Native Drawer surfaces. Motion is Reanimated's (plan N0, Decision 4): these
9
+ * styles draw the panel and backdrop, the animated views around them move them.
10
+ */
11
+ export const nativeStyles = css.create({
12
+ fill: {
13
+ position: 'absolute',
14
+ top: 0,
15
+ left: 0,
16
+ right: 0,
17
+ bottom: 0,
18
+ },
19
+ scrim: {
20
+ backgroundColor: colors.scrim,
21
+ },
22
+ panel: {
23
+ flexGrow: 1,
24
+ backgroundColor: colors.bgCard,
25
+ boxShadow: shadows.lg,
26
+ display: 'flex',
27
+ flexDirection: 'column',
28
+ overflow: 'hidden',
29
+ },
30
+ panelBottom: {
31
+ borderTopWidth: borders.hairline,
32
+ borderTopStyle: 'solid',
33
+ borderTopColor: colors.border,
34
+ borderTopLeftRadius: radii.lg,
35
+ borderTopRightRadius: radii.lg,
36
+ },
37
+ panelRight: {
38
+ borderLeftWidth: borders.hairline,
39
+ borderLeftStyle: 'solid',
40
+ borderLeftColor: colors.border,
41
+ },
42
+ panelLeft: {
43
+ borderRightWidth: borders.hairline,
44
+ borderRightStyle: 'solid',
45
+ borderRightColor: colors.border,
46
+ },
47
+ // The safe-area insets (and a snap sheet's overshoot skirt), as padding.
48
+ insets: (top: number, bottom: number) => ({paddingTop: top, paddingBottom: bottom}),
49
+ // The Body's padding inside its ScrollView (no flex: it sizes to content).
50
+ bodyContent: {
51
+ paddingLeft: spacing.lg,
52
+ paddingRight: spacing.lg,
53
+ paddingBottom: spacing.lg,
54
+ },
55
+ bodyFlush: {
56
+ paddingLeft: 0,
57
+ paddingRight: 0,
58
+ },
59
+ // A snap sheet resting below its largest detent: its lower part is off
60
+ // screen, so the list scrolls that far further to show its end.
61
+ hiddenBelow: (height: number) => ({paddingBottom: height}),
62
+ })