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