@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,491 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ type RefObject,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ } from 'react'
10
+ import {
11
+ type AccessibilityActionEvent,
12
+ type LayoutChangeEvent,
13
+ Pressable,
14
+ StyleSheet,
15
+ useWindowDimensions,
16
+ type View,
17
+ } from 'react-native'
18
+ import {Gesture, GestureDetector, type GestureType} from 'react-native-gesture-handler'
19
+ import Animated, {
20
+ type AnimatedRef,
21
+ Easing,
22
+ scrollTo,
23
+ useAnimatedRef,
24
+ useAnimatedScrollHandler,
25
+ useAnimatedStyle,
26
+ useSharedValue,
27
+ withTiming,
28
+ } from 'react-native-reanimated'
29
+ import {scheduleOnRN} from 'react-native-worklets'
30
+ import {html} from 'react-strict-dom'
31
+ import {DURATION_MS, SIZES_PX} from '@duro-app/tokens/keys'
32
+ import {ModalContext} from '../../shared/ModalContext'
33
+ import {OverlayModal} from '../../shared/portal/OverlayModal'
34
+ import {focusForAccessibility} from '../../shared/accessibilityFocus'
35
+ import {useSafeAreaInsets} from '../../shared/safeArea'
36
+ import {useKeyboardHeight} from '../../shared/useKeyboardHeight'
37
+ import {withPress} from '../../shared/pressableChild'
38
+ import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
39
+ import {Icon} from '../Icon/Icon'
40
+ import {nativeStyles} from './native.css'
41
+ import {partStyles} from './parts.css'
42
+ import {
43
+ DEFAULT_GRABBER_LABELS,
44
+ Description,
45
+ DrawerContext,
46
+ Footer,
47
+ Header,
48
+ Title,
49
+ useDrawer,
50
+ type DrawerSize,
51
+ type GrabberLabels,
52
+ } from './parts'
53
+ import {
54
+ CROSS_SLOP,
55
+ DISMISS_DURATION,
56
+ DRAG_SLOP,
57
+ SNAP_DURATION,
58
+ SNAP_MAX_OVERSHOOT,
59
+ cycleDetent,
60
+ detentHeight,
61
+ rubberBand,
62
+ settleDetent,
63
+ shouldDismissEdge,
64
+ } from './sheetPhysics'
65
+ import {useDrawerRoot, type DrawerRootProps} from './useDrawerRoot'
66
+
67
+ export type {DrawerAnchor, DrawerSize, SnapPoint} from './parts'
68
+
69
+ /** Native-only wiring: where accessibility focus returns on close. */
70
+ const TriggerRefContext = createContext<RefObject<unknown> | null>(null)
71
+
72
+ /** What Drawer.Body needs from its sheet: the scroll the drag defers to. */
73
+ interface SheetScroll {
74
+ scrollRef: AnimatedRef<Animated.ScrollView>
75
+ scrollGesture: GestureType
76
+ onScroll: ReturnType<typeof useAnimatedScrollHandler>
77
+ /** px of a snap sheet below the screen's bottom edge at its detent */
78
+ hiddenBelow: number
79
+ }
80
+
81
+ const SheetScrollContext = createContext<SheetScroll | null>(null)
82
+
83
+ /**
84
+ * Native Drawer (plan N0, Slice B): the same compound API, in an
85
+ * OverlayModal. The panel follows the finger on the UI thread (Gesture
86
+ * Handler + Reanimated); a bottom drawer with `snapPoints` rests at its
87
+ * detents and dismisses below the lowest. The backdrop press and Android's
88
+ * back button close it; the screen reader's focus moves into the panel on
89
+ * open and back to the trigger on close.
90
+ */
91
+ function Root({children, ...props}: DrawerRootProps) {
92
+ const ctx = useDrawerRoot(props)
93
+ const triggerRef = useRef<unknown>(null)
94
+
95
+ // Back to the trigger once the drawer is gone.
96
+ const wasOpen = useRef(ctx.open)
97
+ useEffect(() => {
98
+ if (wasOpen.current && !ctx.open) focusForAccessibility(triggerRef.current, 'Drawer')
99
+ wasOpen.current = ctx.open
100
+ }, [ctx.open])
101
+
102
+ return (
103
+ <DrawerContext.Provider value={ctx}>
104
+ <TriggerRefContext.Provider value={triggerRef}>{children}</TriggerRefContext.Provider>
105
+ </DrawerContext.Provider>
106
+ )
107
+ }
108
+
109
+ /** Opens the drawer from its one child (a Button), whose host gets focus back on close. */
110
+ function Trigger({children}: {children: ReactNode}) {
111
+ const {requestOpen} = useDrawer()
112
+ const triggerRef = useContext(TriggerRefContext)
113
+ return withPress(children, requestOpen, triggerRef ?? undefined)
114
+ }
115
+
116
+ interface PortalProps {
117
+ children: ReactNode
118
+ size?: DrawerSize
119
+ /** The grabber's accessible names, by what a press does next. Snap sheets only */
120
+ grabberLabels?: GrabberLabels
121
+ }
122
+
123
+ const PANEL_WIDTH = {sm: SIZES_PX.panelSm, md: SIZES_PX.panelMd, lg: SIZES_PX.panelLg} as const
124
+
125
+ /** A content-height bottom drawer is capped at 85% of the window, as the web caps it at 85vh. */
126
+ const CONTENT_SHEET_CAP = 0.85
127
+
128
+ const easeOut = Easing.out(Easing.cubic)
129
+ const easeIn = Easing.in(Easing.cubic)
130
+
131
+ function Portal({children, size = 'md', grabberLabels = DEFAULT_GRABBER_LABELS}: PortalProps) {
132
+ const {
133
+ open,
134
+ closing,
135
+ closeDuration,
136
+ anchor,
137
+ dismissable,
138
+ swipeDismiss,
139
+ requestClose,
140
+ requestCloseImmediate,
141
+ titleId,
142
+ panelRef,
143
+ snapPoints,
144
+ snapIndex,
145
+ setSnapIndex,
146
+ } = useDrawer()
147
+ const window = useWindowDimensions()
148
+ const insets = useSafeAreaInsets()
149
+ // A bottom drawer rides above the on-screen keyboard: its detents measure
150
+ // from the keyboard's top edge, so a search field and its results stay seen.
151
+ const keyboard = useKeyboardHeight()
152
+ const lift = anchor === 'bottom' ? keyboard : 0
153
+ const availableHeight = window.height - lift
154
+ const bottom = anchor === 'bottom'
155
+ const snapping = snapPoints.length > 0
156
+
157
+ const heights = useMemo(
158
+ () => snapPoints.map((p) => detentHeight(p, availableHeight, insets.top)),
159
+ [snapPoints, availableHeight, insets.top],
160
+ )
161
+ const high = heights.length > 0 ? heights[heights.length - 1] : 0
162
+ // Where the panel rests at its detent: its offset below its own full height.
163
+ const rest = snapping ? high - heights[snapIndex] : 0
164
+ // Off screen: the whole panel past its edge (a snap sheet's overshoot skirt included).
165
+ const hidden = snapping ? high + SNAP_MAX_OVERSHOOT : bottom ? window.height : window.width
166
+
167
+ // The panel's displacement toward its edge, in px: 0 shows it whole; a
168
+ // snap sheet rests at restOf(detent); negative only while pulled past its
169
+ // largest detent.
170
+ const offset = useSharedValue(hidden)
171
+ const start = useSharedValue(0)
172
+ const extent = useSharedValue(bottom ? window.height : window.width)
173
+ const index = useSharedValue(snapIndex)
174
+ const owns = useSharedValue(false)
175
+ const inBody = useSharedValue(false)
176
+ const scrollY = useSharedValue(0)
177
+ const scrollRef = useAnimatedRef<Animated.ScrollView>()
178
+
179
+ // Enter, exit, and a detent chosen from React (the grabber): animate there.
180
+ useEffect(() => {
181
+ if (!open) {
182
+ offset.value = hidden
183
+ return
184
+ }
185
+ if (closing) {
186
+ offset.value = withTiming(hidden, {duration: DURATION_MS[closeDuration], easing: easeIn})
187
+ return
188
+ }
189
+ index.value = snapIndex
190
+ offset.value = withTiming(rest, {duration: DURATION_MS.base, easing: easeOut})
191
+ // `rest` and `hidden` follow the window: a rotation re-seats the panel too.
192
+ }, [open, closing, closeDuration, snapIndex, rest, hidden, index, offset])
193
+
194
+ const heightsShared = useSharedValue(heights)
195
+ useEffect(() => {
196
+ heightsShared.value = heights
197
+ }, [heights, heightsShared])
198
+
199
+ const scrollGesture = useMemo(
200
+ () =>
201
+ Gesture.Native().onBegin(() => {
202
+ inBody.value = true
203
+ }),
204
+ [inBody],
205
+ )
206
+
207
+ const onScroll = useAnimatedScrollHandler((e) => {
208
+ scrollY.value = e.contentOffset.y
209
+ })
210
+
211
+ const horizontalSign = anchor === 'left' ? -1 : 1
212
+ const canDismiss = swipeDismiss && dismissable
213
+ const pan = useMemo(() => {
214
+ const toEdge = (x: number, y: number) => {
215
+ 'worklet'
216
+ return bottom ? y : x * horizontalSign
217
+ }
218
+ const settleTo = (target: number) => {
219
+ 'worklet'
220
+ offset.value = withTiming(target, {duration: SNAP_DURATION, easing: easeOut})
221
+ }
222
+ const dismiss = () => {
223
+ 'worklet'
224
+ offset.value = withTiming(hidden, {duration: DISMISS_DURATION, easing: easeIn}, (done) => {
225
+ if (done) scheduleOnRN(requestCloseImmediate)
226
+ })
227
+ }
228
+ const gesture = Gesture.Pan()
229
+ .enabled(swipeDismiss || snapping)
230
+ .onStart((e) => {
231
+ start.value = offset.value
232
+ const t = toEdge(e.translationX, e.translationY)
233
+ // From the content, the sheet takes the drag only with the list at its
234
+ // top: toward the edge, or upward while a larger detent remains.
235
+ owns.value =
236
+ !bottom || !inBody.value
237
+ ? true
238
+ : scrollY.value <= 0 &&
239
+ (t > 0 || (snapping && index.value < heightsShared.value.length - 1))
240
+ })
241
+ .onUpdate((e) => {
242
+ if (!owns.value) return
243
+ if (inBody.value) scrollTo(scrollRef, 0, 0, false)
244
+ const next = start.value + toEdge(e.translationX, e.translationY)
245
+ if (snapping) offset.value = next < 0 ? -rubberBand(-next, SNAP_MAX_OVERSHOOT) : next
246
+ else offset.value = Math.max(0, next)
247
+ })
248
+ .onEnd((e) => {
249
+ if (!owns.value) return
250
+ const velocity = toEdge(e.velocityX, e.velocityY) / 1000
251
+ if (snapping) {
252
+ const hs = heightsShared.value
253
+ const top = hs[hs.length - 1]
254
+ const settled = settleDetent({
255
+ heights: hs,
256
+ from: index.value,
257
+ height: top - offset.value,
258
+ velocity,
259
+ })
260
+ if (settled === 'dismiss') {
261
+ if (canDismiss) dismiss()
262
+ else settleTo(top - hs[0])
263
+ return
264
+ }
265
+ settleTo(top - hs[settled])
266
+ if (settled !== index.value) {
267
+ index.value = settled
268
+ scheduleOnRN(setSnapIndex, settled)
269
+ }
270
+ return
271
+ }
272
+ if (canDismiss && shouldDismissEdge(offset.value, extent.value, velocity)) dismiss()
273
+ else settleTo(0)
274
+ })
275
+ .onFinalize(() => {
276
+ inBody.value = false
277
+ owns.value = false
278
+ })
279
+ .simultaneousWithExternalGesture(scrollGesture)
280
+ return bottom
281
+ ? gesture.activeOffsetY([-DRAG_SLOP, DRAG_SLOP]).failOffsetX([-CROSS_SLOP, CROSS_SLOP])
282
+ : gesture.activeOffsetX([-DRAG_SLOP, DRAG_SLOP]).failOffsetY([-CROSS_SLOP, CROSS_SLOP])
283
+ }, [
284
+ bottom,
285
+ horizontalSign,
286
+ snapping,
287
+ swipeDismiss,
288
+ canDismiss,
289
+ hidden,
290
+ requestCloseImmediate,
291
+ setSnapIndex,
292
+ scrollGesture,
293
+ offset,
294
+ start,
295
+ extent,
296
+ index,
297
+ owns,
298
+ inBody,
299
+ scrollY,
300
+ scrollRef,
301
+ heightsShared,
302
+ ])
303
+
304
+ const panelMotion = useAnimatedStyle(() =>
305
+ bottom
306
+ ? {transform: [{translateY: offset.value}]}
307
+ : {transform: [{translateX: offset.value * horizontalSign}]},
308
+ )
309
+ const backdropMotion = useAnimatedStyle(() => {
310
+ if (snapping) {
311
+ const hs = heightsShared.value
312
+ const visible = hs[hs.length - 1] - offset.value
313
+ return {opacity: Math.min(1, Math.max(0, visible / hs[0]))}
314
+ }
315
+ return {opacity: Math.min(1, Math.max(0, 1 - offset.value / extent.value))}
316
+ })
317
+
318
+ const onPanelLayout = (e: LayoutChangeEvent) => {
319
+ const {width, height} = e.nativeEvent.layout
320
+ extent.value = bottom ? height : width
321
+ }
322
+
323
+ const placement = bottom
324
+ ? snapping
325
+ ? [layout.bottom, {height: high + SNAP_MAX_OVERSHOOT, bottom: lift - SNAP_MAX_OVERSHOOT}]
326
+ : [layout.bottom, {bottom: lift, maxHeight: availableHeight * CONTENT_SHEET_CAP}]
327
+ : [layout.side, anchor === 'left' ? layout.left : layout.right, {maxWidth: PANEL_WIDTH[size]}]
328
+ const padTop = bottom ? 0 : insets.top
329
+ const padBottom = (lift > 0 ? 0 : insets.bottom) + (snapping ? SNAP_MAX_OVERSHOOT : 0)
330
+
331
+ const nextIndex = cycleDetent(snapIndex, snapPoints.length)
332
+ const onGrabberAction = (e: AccessibilityActionEvent) => {
333
+ if (e.nativeEvent.actionName === 'increment') {
334
+ setSnapIndex(Math.min(snapIndex + 1, snapPoints.length - 1))
335
+ } else if (e.nativeEvent.actionName === 'decrement') {
336
+ setSnapIndex(Math.max(snapIndex - 1, 0))
337
+ }
338
+ }
339
+
340
+ const sheetScroll: SheetScroll = {
341
+ scrollRef,
342
+ scrollGesture,
343
+ onScroll,
344
+ hiddenBelow: snapping ? high - heights[snapIndex] : 0,
345
+ }
346
+
347
+ return (
348
+ <OverlayModal
349
+ visible={open}
350
+ onRequestClose={() => {
351
+ if (dismissable) requestClose()
352
+ }}
353
+ onShow={() => focusForAccessibility(panelRef.current, 'Drawer')}
354
+ >
355
+ <Animated.View style={[StyleSheet.absoluteFill, backdropMotion]}>
356
+ <Pressable
357
+ style={StyleSheet.absoluteFill}
358
+ onPress={dismissable ? requestClose : undefined}
359
+ accessible={false}
360
+ importantForAccessibility="no"
361
+ >
362
+ <html.div aria-hidden style={[nativeStyles.fill, nativeStyles.scrim]} />
363
+ </Pressable>
364
+ </Animated.View>
365
+ <GestureDetector gesture={pan}>
366
+ <Animated.View
367
+ ref={panelRef as RefObject<View | null>}
368
+ accessibilityViewIsModal
369
+ onLayout={onPanelLayout}
370
+ style={[placement, panelMotion]}
371
+ >
372
+ <html.div
373
+ role="dialog"
374
+ aria-modal
375
+ aria-labelledby={titleId}
376
+ style={[
377
+ nativeStyles.panel,
378
+ bottom && nativeStyles.panelBottom,
379
+ anchor === 'right' && nativeStyles.panelRight,
380
+ anchor === 'left' && nativeStyles.panelLeft,
381
+ nativeStyles.insets(padTop, padBottom),
382
+ ]}
383
+ >
384
+ {snapping && (
385
+ <Pressable
386
+ accessibilityRole="adjustable"
387
+ accessibilityLabel={
388
+ nextIndex > snapIndex ? grabberLabels.expand : grabberLabels.collapse
389
+ }
390
+ accessibilityValue={{text: snapPoints[snapIndex]}}
391
+ accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
392
+ onAccessibilityAction={onGrabberAction}
393
+ onPress={() => setSnapIndex(nextIndex)}
394
+ >
395
+ <html.div style={partStyles.grabberHit}>
396
+ <html.div style={partStyles.grabber} />
397
+ </html.div>
398
+ </Pressable>
399
+ )}
400
+ <ModalContext.Provider value={true}>
401
+ <ControlContextBoundary>
402
+ <SheetScrollContext.Provider value={sheetScroll}>
403
+ {children}
404
+ </SheetScrollContext.Provider>
405
+ </ControlContextBoundary>
406
+ </ModalContext.Provider>
407
+ </html.div>
408
+ </Animated.View>
409
+ </GestureDetector>
410
+ </OverlayModal>
411
+ )
412
+ }
413
+
414
+ // Layout-only RN styles: where the moving panel sits before it moves.
415
+ const layout = StyleSheet.create({
416
+ bottom: {position: 'absolute', left: 0, right: 0, bottom: 0},
417
+ side: {position: 'absolute', top: 0, bottom: 0, width: '100%'},
418
+ left: {left: 0},
419
+ right: {right: 0},
420
+ body: {flexGrow: 0, flexShrink: 1},
421
+ bodyFill: {flex: 1},
422
+ })
423
+
424
+ /**
425
+ * Native Body: a ScrollView, since a native div does not scroll its
426
+ * overflow. In a bottom sheet the drag defers to it: the list scrolls, and
427
+ * moves the sheet only from its top.
428
+ */
429
+ interface BodyProps {
430
+ children: ReactNode
431
+ /** No side padding: rows that run to the panel's edges, as a picker's list. */
432
+ flush?: boolean
433
+ }
434
+
435
+ function Body({children, flush = false}: BodyProps) {
436
+ const sheet = useContext(SheetScrollContext)
437
+ const {snapPoints} = useDrawer()
438
+ if (!sheet) return null
439
+ return (
440
+ <GestureDetector gesture={sheet.scrollGesture}>
441
+ <Animated.ScrollView
442
+ ref={sheet.scrollRef}
443
+ onScroll={sheet.onScroll}
444
+ scrollEventThrottle={16}
445
+ bounces={false}
446
+ // Presses inside the sheet keep the keyboard up. A ScrollView above the
447
+ // sheet's Modal can still take the first touch, which is why picker
448
+ // rows use Gesture Handler's Pressable (Listbox.native).
449
+ keyboardShouldPersistTaps="handled"
450
+ style={snapPoints.length > 0 ? layout.bodyFill : layout.body}
451
+ >
452
+ <html.div
453
+ style={[
454
+ nativeStyles.bodyContent,
455
+ flush && nativeStyles.bodyFlush,
456
+ nativeStyles.hiddenBelow(sheet.hiddenBelow),
457
+ ]}
458
+ >
459
+ {children}
460
+ </html.div>
461
+ </Animated.ScrollView>
462
+ </GestureDetector>
463
+ )
464
+ }
465
+
466
+ interface CloseProps {
467
+ children?: ReactNode
468
+ 'aria-label'?: string
469
+ }
470
+
471
+ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
472
+ const {requestClose} = useDrawer()
473
+ if (children) return withPress(children, requestClose)
474
+ return (
475
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={partStyles.closeButton}>
476
+ <Icon name="x" size="sm" color="textMuted" />
477
+ </html.button>
478
+ )
479
+ }
480
+
481
+ export const Drawer = {
482
+ Root,
483
+ Trigger,
484
+ Portal,
485
+ Header,
486
+ Title,
487
+ Description,
488
+ Body,
489
+ Footer,
490
+ Close,
491
+ }