@oxyhq/bloom 0.30.4 → 0.30.6

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 (66) hide show
  1. package/lib/commonjs/avatar/Avatar.js +4 -2
  2. package/lib/commonjs/avatar/Avatar.js.map +1 -1
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +744 -0
  4. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -0
  5. package/lib/commonjs/bottom-sheet/index.js +24 -694
  6. package/lib/commonjs/bottom-sheet/index.js.map +1 -1
  7. package/lib/commonjs/bottom-sheet/index.web.js +64 -0
  8. package/lib/commonjs/bottom-sheet/index.web.js.map +1 -0
  9. package/lib/commonjs/dialog/Dialog.js +2 -2
  10. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  11. package/lib/commonjs/dialog/DialogBottomSheet.js +2 -2
  12. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  13. package/lib/commonjs/dialog/SheetShell.js +2 -2
  14. package/lib/commonjs/dialog/SheetShell.js.map +1 -1
  15. package/lib/commonjs/index.js +44 -44
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/index.web.js +66 -66
  18. package/lib/commonjs/index.web.js.map +1 -1
  19. package/lib/module/avatar/Avatar.js +4 -2
  20. package/lib/module/avatar/Avatar.js.map +1 -1
  21. package/lib/module/bottom-sheet/BottomSheetBase.js +741 -0
  22. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -0
  23. package/lib/module/bottom-sheet/index.js +27 -696
  24. package/lib/module/bottom-sheet/index.js.map +1 -1
  25. package/lib/module/bottom-sheet/index.web.js +63 -0
  26. package/lib/module/bottom-sheet/index.web.js.map +1 -0
  27. package/lib/module/dialog/Dialog.js +1 -1
  28. package/lib/module/dialog/Dialog.js.map +1 -1
  29. package/lib/module/dialog/DialogBottomSheet.js +1 -1
  30. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  31. package/lib/module/dialog/SheetShell.js +1 -1
  32. package/lib/module/dialog/SheetShell.js.map +1 -1
  33. package/lib/module/index.js +1 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/index.web.js +1 -1
  36. package/lib/module/index.web.js.map +1 -1
  37. package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/avatar/types.d.ts +4 -3
  39. package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +127 -0
  41. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -0
  42. package/lib/typescript/commonjs/bottom-sheet/index.d.ts +2 -90
  43. package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts +7 -0
  45. package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  47. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  48. package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
  49. package/lib/typescript/module/avatar/types.d.ts +4 -3
  50. package/lib/typescript/module/avatar/types.d.ts.map +1 -1
  51. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +127 -0
  52. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -0
  53. package/lib/typescript/module/bottom-sheet/index.d.ts +2 -90
  54. package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
  55. package/lib/typescript/module/bottom-sheet/index.web.d.ts +7 -0
  56. package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -0
  57. package/lib/typescript/module/index.web.d.ts +2 -2
  58. package/lib/typescript/module/index.web.d.ts.map +1 -1
  59. package/package.json +6 -1
  60. package/src/__tests__/BottomSheetWebFork.test.ts +66 -0
  61. package/src/avatar/Avatar.tsx +4 -2
  62. package/src/avatar/types.ts +4 -3
  63. package/src/bottom-sheet/BottomSheetBase.tsx +866 -0
  64. package/src/bottom-sheet/index.tsx +26 -821
  65. package/src/bottom-sheet/index.web.tsx +66 -0
  66. package/src/index.web.ts +2 -2
@@ -1,15 +1,13 @@
1
1
  "use strict";
2
2
 
3
- import { forwardRef, useImperativeHandle, useRef, useEffect, useState, useCallback, useMemo } from 'react';
4
- import { View, StyleSheet, Modal, Pressable, Dimensions, Platform } from 'react-native';
5
- import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
6
- import { Z_INDEX } from "../styles/z-index.js";
7
- import Animated, { interpolate, runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
8
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
- import { useTheme } from "../theme/use-theme.js";
3
+ import { forwardRef } from 'react';
4
+ import { Modal, Platform, StyleSheet } from 'react-native';
5
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
6
+ import { BottomSheetBase } from "./BottomSheetBase.js";
10
7
 
11
8
  // Keyboard handling — only on native platforms. On web, keyboard events are
12
- // handled by the browser. `react-native-keyboard-controller` is an OPTIONAL
9
+ // handled by the browser (and the web build resolves to `index.web.tsx`, which
10
+ // never imports this module). `react-native-keyboard-controller` is an OPTIONAL
13
11
  // dependency: it is lazily `require`d below and everything no-ops when it is
14
12
  // absent (or on web).
15
13
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -74,712 +72,45 @@ function SheetKeyboardSync({
74
72
  return null;
75
73
  }
76
74
 
77
- /** Hook that returns current screen dimensions and updates on rotation/resize. */
78
- function useScreenDimensions() {
79
- const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
80
- useEffect(() => {
81
- const subscription = Dimensions.addEventListener('change', ({
82
- window
83
- }) => {
84
- setDimensions(window);
85
- });
86
- return () => subscription.remove();
87
- }, []);
88
- return dimensions;
89
- }
90
- const SPRING_CONFIG = {
91
- damping: 25,
92
- stiffness: 300,
93
- mass: 0.8
94
- };
95
- const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
96
- const {
97
- children,
98
- onDismiss,
99
- enablePanDownToClose = true,
100
- backgroundComponent,
101
- backdropComponent,
102
- style,
103
- enableHandlePanningGesture = true,
104
- onDismissAttempt,
105
- detached = false,
106
- showHandle = true,
107
- backdropOpacity = 0.5,
108
- scrollable = true,
109
- manualActivation = false,
110
- dynamicBackdrop = false,
111
- handleComponent
112
- } = props;
113
- const insets = useSafeAreaInsets();
114
- const theme = useTheme();
115
- const {
116
- colors
117
- } = theme;
118
- const {
119
- height: screenHeight
120
- } = useScreenDimensions();
121
- const [visible, setVisible] = useState(false);
122
- const [rendered, setRendered] = useState(false); // keep mounted for exit animation
123
- const closeTimeoutRef = useRef(null);
124
- const hasClosedRef = useRef(false);
125
- const scrollViewRef = useRef(null);
126
- /**
127
- * Monotonically increasing counter that identifies "the current close
128
- * attempt". Bumped every time the sheet re-opens (`present()`), so any
129
- * in-flight `withTiming` completion callback or fallback timer from a
130
- * PREVIOUS close cycle becomes a no-op. Without this guard, a stale
131
- * `runOnJS(finishClose)` from an aborted close would fire `onDismiss`
132
- * and unmount the sheet immediately after the user opens it again,
133
- * causing "tap to open does nothing" reports in production.
134
- */
135
- const closeGenerationRef = useRef(0);
136
- const screenHeightSV = useSharedValue(screenHeight);
137
- // Keep shared value in sync when screen dimensions change (rotation/resize)
138
- useEffect(() => {
139
- screenHeightSV.value = screenHeight;
140
- }, [screenHeight, screenHeightSV]);
141
- const translateY = useSharedValue(screenHeight);
142
- const opacity = useSharedValue(0);
143
- const scrollOffsetY = useSharedValue(0);
144
- const isScrollAtTop = useSharedValue(true);
145
- const allowPanClose = useSharedValue(true);
146
- const keyboardHeight = useSharedValue(0);
147
- // Stores the sheet's `translateY` captured at the moment a pan gesture
148
- // begins, so `onUpdate` can offset from the starting position. This MUST
149
- // be a primitive shared value, not an object-valued one. Under
150
- // react-native-worklets 0.8.x (Reanimated 4), assigning a fresh object
151
- // literal to an object-valued shared value's `.value` inside a worklet
152
- // freezes the object (`freezeObjectInDev`) and routes the mutation through
153
- // the serializer's listener machinery — which then tries to call the
154
- // non-worklet `removeListener` synchronously on the UI thread and crashes
155
- // ("Tried to synchronously call a non-worklet function `removeListener`").
156
- // A scalar shared value sidesteps all object serialization.
157
- const contextY = useSharedValue(0);
158
- // Mirror of `closeGenerationRef` for worklet access. Bumped from the JS
159
- // thread in lockstep with the ref so gesture worklets always see the
160
- // current generation when they snapshot it on `onEnd`.
161
- const closeGeneration = useSharedValue(0);
162
- // Used by `manualActivation` body pan to track the touch's initial Y so
163
- // it can compute downward distance for the activation decision.
164
- const touchStartY = useSharedValue(0);
165
-
166
- // Refs used to mark the handle pan and the body pan as mutually
167
- // simultaneous (manualActivation mode only). Without this RNGH treats them
168
- // as racing gestures and a touch that begins in the handle area could be
169
- // claimed by whichever recognizer activates first — leading to
170
- // inconsistent drag start.
171
- const bodyPanRef = useRef(undefined);
172
- const handlePanRef = useRef(undefined);
173
-
174
- // The sheet's keyboard-height tracker is registered by <SheetKeyboardSync>,
175
- // rendered INSIDE the Modal under the re-established <KeyboardProvider> (see
176
- // the render tree below). It must NOT be called here in the component body:
177
- // this body runs OUTSIDE the Modal, where the keyboard-controller provider
178
- // that actually receives the Modal window's insets is not reachable.
179
-
180
- // Dismiss callbacks
181
- const safeClose = useCallback(() => {
182
- if (onDismissAttempt?.()) {
183
- onDismiss?.();
184
- } else if (!onDismissAttempt) {
185
- onDismiss?.();
186
- }
187
- }, [onDismissAttempt, onDismiss]);
188
-
189
- // Mirror `safeClose` and `rendered` into refs so the unmount cleanup can
190
- // fire the latest dismiss callback when needed, without re-binding the
191
- // cleanup effect on every render.
192
- const safeCloseRef = useRef(safeClose);
193
- useEffect(() => {
194
- safeCloseRef.current = safeClose;
195
- }, [safeClose]);
196
- const renderedRef = useRef(rendered);
197
- useEffect(() => {
198
- renderedRef.current = rendered;
199
- }, [rendered]);
200
-
201
- /**
202
- * Commit a close. Two guards prevent stale callbacks from firing:
203
- * 1. `hasClosedRef` — protects against the fallback timer AND the
204
- * animation callback both racing to call us within a single close
205
- * cycle.
206
- * 2. `generation` — protects against a callback from a PREVIOUS close
207
- * cycle firing AFTER the user reopened. If the live generation has
208
- * advanced past the one captured when the close started, this
209
- * callback is from a cycle that the user has implicitly cancelled
210
- * by reopening — silently drop it.
211
- */
212
- const finishClose = useCallback(generation => {
213
- if (closeGenerationRef.current !== generation) return;
214
- if (hasClosedRef.current) return;
215
- hasClosedRef.current = true;
216
- safeClose();
217
- setRendered(false);
218
- }, [safeClose]);
219
- useEffect(() => {
220
- if (visible) {
221
- if (closeTimeoutRef.current) {
222
- clearTimeout(closeTimeoutRef.current);
223
- closeTimeoutRef.current = null;
224
- }
225
- hasClosedRef.current = false;
226
- // Bump generation: any pending close-completion callback from a
227
- // prior cycle (animation or fallback timer) will now no-op when
228
- // it eventually fires, because its captured generation is stale.
229
- closeGenerationRef.current += 1;
230
- closeGeneration.value = closeGenerationRef.current;
231
- opacity.value = withTiming(1, {
232
- duration: 250
233
- });
234
- translateY.value = withSpring(0, SPRING_CONFIG);
235
- } else if (rendered) {
236
- // Capture the generation for THIS close cycle so the animation
237
- // callback (running on the UI thread, scheduled back to JS) and
238
- // the fallback timer agree on which cycle they belong to.
239
- const generation = closeGenerationRef.current;
240
- opacity.value = withTiming(0, {
241
- duration: 250
242
- }, finished => {
243
- if (finished) {
244
- runOnJS(finishClose)(generation);
245
- }
246
- });
247
- translateY.value = withSpring(screenHeight, {
248
- ...SPRING_CONFIG,
249
- stiffness: 250
250
- });
251
-
252
- // Fallback timer to ensure close completes (especially on web
253
- // where reanimated callbacks occasionally drop on tab blur).
254
- if (closeTimeoutRef.current) {
255
- clearTimeout(closeTimeoutRef.current);
256
- }
257
- closeTimeoutRef.current = setTimeout(() => {
258
- finishClose(generation);
259
- closeTimeoutRef.current = null;
260
- }, 300);
261
- }
262
- }, [visible, rendered, finishClose, screenHeight, closeGeneration, opacity, translateY]);
263
-
264
- // On unmount: ensure pending close callbacks (e.g. consumer's `onDismiss`)
265
- // still fire if the BS is yanked mid-animation by a parent re-render.
266
- // Without this, `Dialog`'s `handleDismiss` never runs and queued
267
- // callbacks (post-close handlers) are silently lost.
268
- // Only fires when the sheet was actually rendered (open or closing) to
269
- // avoid spuriously calling onDismiss on bare unmount of a never-opened
270
- // sheet. Refs are read inside the cleanup, so latest values are captured.
271
- useEffect(() => () => {
272
- if (closeTimeoutRef.current) {
273
- clearTimeout(closeTimeoutRef.current);
274
- closeTimeoutRef.current = null;
275
- }
276
- if (renderedRef.current && !hasClosedRef.current) {
277
- hasClosedRef.current = true;
278
- safeCloseRef.current();
279
- }
280
- }, []);
281
-
282
- // Apply web scrollbar styles when colors change; clean up on unmount
283
- useEffect(() => {
284
- if (Platform.OS === 'web') {
285
- createWebScrollbarStyle(colors.border);
286
- }
287
- return () => {
288
- removeWebScrollbarStyle();
289
- };
290
- }, [colors.border]);
291
- const present = useCallback(() => {
292
- setRendered(true);
293
- setVisible(true);
294
- }, []);
295
- const dismiss = useCallback(() => {
296
- setVisible(false);
297
- }, []);
298
- const scrollTo = useCallback((y, animated = true) => {
299
- scrollViewRef.current?.scrollTo({
300
- y,
301
- animated
302
- });
303
- }, []);
304
- useImperativeHandle(ref, () => ({
305
- present,
306
- dismiss,
307
- close: dismiss,
308
- expand: present,
309
- collapse: dismiss,
310
- scrollTo
311
- }), [present, dismiss, scrollTo]);
312
- const nativeGesture = useMemo(() => Gesture.Native(), []);
313
-
314
- // Body pan — two strategies, switched by `manualActivation`.
315
- //
316
- // (1) Legacy mode (`manualActivation: false`, the bloom 0.3.x default):
317
- // Pan is always active. We snapshot the scroll offset at `onStart`
318
- // and gate movement on `scrollOffsetY <= 8` during the gesture.
319
- // This is what current bloom consumers expect.
320
- //
321
- // (2) gorhom-style mode (`manualActivation: true`):
322
- // Pan uses `manualActivation(true)` and only flips to active when
323
- // the inner ScrollView is at the top AND the user has moved their
324
- // finger downward > 8dp. In every other case it fails, so the
325
- // ScrollView keeps full ownership of the touch. This is the only
326
- // RNGH 2.x pattern that does not steal vertical events from the
327
- // inner scroller on Android. Required for FileManagement /
328
- // PhotoPicker style sheets.
329
- const panGesture = useMemo(() => {
330
- if (manualActivation) {
331
- return Gesture.Pan().enabled(enablePanDownToClose).withRef(bodyPanRef).manualActivation(true).simultaneousWithExternalGesture(scrollViewRef, handlePanRef).onTouchesDown(e => {
332
- 'worklet';
333
-
334
- const t = e.changedTouches[0];
335
- if (t) touchStartY.value = t.absoluteY;
336
- contextY.value = translateY.value;
337
- }).onTouchesMove((e, state) => {
338
- 'worklet';
339
-
340
- const t = e.changedTouches[0];
341
- if (!t) return;
342
- const dy = t.absoluteY - touchStartY.value;
343
- const atTop = scrollOffsetY.value <= 4;
344
- // Activate only when (at scroll top) AND (finger has moved
345
- // downward by > 8dp). Any other motion: fail so the
346
- // ScrollView claims the gesture.
347
- if (atTop && dy > 8) {
348
- state.activate();
349
- } else if (dy < -4 || !atTop) {
350
- state.fail();
351
- }
352
- }).onUpdate(event => {
353
- 'worklet';
354
-
355
- if (event.translationY < 0) return;
356
- const newTranslateY = contextY.value + event.translationY;
357
- if (newTranslateY >= 0) {
358
- translateY.value = newTranslateY;
359
- }
360
- }).onEnd(event => {
361
- 'worklet';
362
-
363
- const velocity = event.velocityY;
364
- const distance = translateY.value;
365
- const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
366
- const fastSwipeThreshold = 900;
367
- const shouldClose = velocity > fastSwipeThreshold || distance > closeThreshold && velocity > -300;
368
- if (shouldClose) {
369
- // Snapshot the generation on the UI thread at the
370
- // moment the close gesture commits. The completion
371
- // callback only fires `finishClose` if no reopen
372
- // bumped the generation in between.
373
- const generation = closeGeneration.value;
374
- translateY.value = withSpring(screenHeightSV.value, {
375
- ...SPRING_CONFIG,
376
- velocity
377
- });
378
- opacity.value = withTiming(0, {
379
- duration: 250
380
- }, finished => {
381
- if (finished) runOnJS(finishClose)(generation);
382
- });
383
- } else {
384
- translateY.value = withSpring(0, {
385
- ...SPRING_CONFIG,
386
- velocity
387
- });
388
- }
389
- });
390
- }
391
-
392
- // Legacy always-active pan (bloom 0.3.x behaviour).
393
- return Gesture.Pan().enabled(enablePanDownToClose).simultaneousWithExternalGesture(nativeGesture).onStart(() => {
394
- 'worklet';
395
-
396
- contextY.value = translateY.value;
397
- allowPanClose.value = scrollOffsetY.value <= 8;
398
- }).onUpdate(event => {
399
- 'worklet';
400
-
401
- if (!allowPanClose.value) {
402
- return;
403
- }
404
- const newTranslateY = contextY.value + event.translationY;
405
- // If user is scrolling down while content isn't at (or near) the top, let ScrollView handle it
406
- const atTopOrNearTop = scrollOffsetY.value <= 8; // slightly larger tolerance for smoother handoff
407
- if (event.translationY > 0 && !atTopOrNearTop) {
408
- return;
409
- }
410
- if (newTranslateY >= 0) {
411
- translateY.value = newTranslateY;
412
- } else if (detached) {
413
- // Only allow overdrag (pulling up beyond top) when detached
414
- translateY.value = newTranslateY * 0.3;
415
- } else {
416
- // In normal mode, prevent overdrag - clamp to 0
417
- translateY.value = 0;
418
- }
419
- }).onEnd(event => {
420
- 'worklet';
421
-
422
- if (!allowPanClose.value) {
423
- return;
424
- }
425
- const velocity = event.velocityY;
426
- const distance = translateY.value;
427
- // Require a deeper pull to close (more like native bottom sheets)
428
- const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
429
- const fastSwipeThreshold = 900;
430
- const shouldClose = velocity > fastSwipeThreshold || distance > closeThreshold && velocity > -300;
431
- if (shouldClose) {
432
- const generation = closeGeneration.value;
433
- translateY.value = withSpring(screenHeightSV.value, {
434
- ...SPRING_CONFIG,
435
- velocity: velocity
436
- });
437
- opacity.value = withTiming(0, {
438
- duration: 250
439
- }, finished => {
440
- if (finished) {
441
- runOnJS(finishClose)(generation);
442
- }
443
- });
444
- } else {
445
- translateY.value = withSpring(0, {
446
- ...SPRING_CONFIG,
447
- velocity: velocity
448
- });
449
- }
450
- });
451
- // Shared values are stable refs; the listed deps are the only JS-side
452
- // values that change the gesture's behavior. `finishClose` is stable
453
- // (useCallback with stable deps).
454
- // eslint-disable-next-line react-hooks/exhaustive-deps
455
- }, [enablePanDownToClose, detached, manualActivation, nativeGesture, finishClose]);
456
-
457
- // Dedicated handle pan — only built in `manualActivation` mode. Always
458
- // active so users can drag the handle even while content is mid-scroll.
459
- // In legacy mode the body pan already wraps the whole sheet (handle
460
- // included), so no separate gesture is needed.
461
- const handlePanGesture = useMemo(() => {
462
- if (!manualActivation) return undefined;
463
- return Gesture.Pan().enabled(enablePanDownToClose && enableHandlePanningGesture).withRef(handlePanRef).simultaneousWithExternalGesture(bodyPanRef).activeOffsetY([-8, 8]).onStart(() => {
464
- 'worklet';
465
-
466
- contextY.value = translateY.value;
467
- }).onUpdate(event => {
468
- 'worklet';
469
-
470
- const newTranslateY = contextY.value + event.translationY;
471
- if (newTranslateY >= 0) {
472
- translateY.value = newTranslateY;
473
- } else if (detached) {
474
- translateY.value = newTranslateY * 0.3;
475
- } else {
476
- translateY.value = 0;
477
- }
478
- }).onEnd(event => {
479
- 'worklet';
480
-
481
- const velocity = event.velocityY;
482
- const distance = translateY.value;
483
- const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
484
- const fastSwipeThreshold = 900;
485
- const shouldClose = velocity > fastSwipeThreshold || distance > closeThreshold && velocity > -300;
486
- if (shouldClose) {
487
- const generation = closeGeneration.value;
488
- translateY.value = withSpring(screenHeightSV.value, {
489
- ...SPRING_CONFIG,
490
- velocity
491
- });
492
- opacity.value = withTiming(0, {
493
- duration: 250
494
- }, finished => {
495
- if (finished) runOnJS(finishClose)(generation);
496
- });
497
- } else {
498
- translateY.value = withSpring(0, {
499
- ...SPRING_CONFIG,
500
- velocity
501
- });
502
- }
503
- });
504
- // eslint-disable-next-line react-hooks/exhaustive-deps
505
- }, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, finishClose]);
506
-
507
- // `opacity.value` drives the fade in/out (0 -> 1). `backdropOpacity` is the
508
- // final dim level once fully visible. We multiply so the consumer-provided
509
- // dim opacity applies smoothly across the animation. When
510
- // `dynamicBackdrop` is enabled, the dim also fades proportionally with
511
- // drag distance (iOS Photos style).
512
- const backdropStyle = useAnimatedStyle(() => {
513
- const dragFactor = dynamicBackdrop ? interpolate(translateY.value, [0, screenHeightSV.value * 0.4], [1, 0.3], 'clamp') : 1;
514
- return {
515
- opacity: opacity.value * backdropOpacity * dragFactor
516
- };
517
- }, [backdropOpacity, dynamicBackdrop]);
518
- const sheetStyle = useAnimatedStyle(() => {
519
- const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
520
- return {
521
- transform: [{
522
- translateY: translateY.value - keyboardHeight.value
523
- }, {
524
- scale
525
- }]
526
- };
527
- }, []);
528
- const sheetHeightStyle = useAnimatedStyle(() => ({
529
- maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0)
530
- }), [insets.top, insets.bottom, detached]);
531
- const sheetMarginStyle = useAnimatedStyle(() => {
532
- // Only add margin when detached, otherwise extend behind safe area
533
- if (detached) {
534
- return {
535
- marginBottom: keyboardHeight.value > 0 ? 16 : insets.bottom + 16
536
- };
537
- }
538
- return {
539
- marginBottom: 0
540
- };
541
- }, [insets.bottom, detached]);
542
- const handleBackdropPress = useCallback(() => {
543
- // Always animate close on backdrop press
544
- if (onDismissAttempt && !onDismissAttempt()) {
545
- return;
546
- }
547
- dismiss();
548
- }, [onDismissAttempt, dismiss]);
549
- const scrollHandler = useAnimatedScrollHandler({
550
- onScroll: event => {
551
- scrollOffsetY.value = event.contentOffset.y;
552
- isScrollAtTop.value = event.contentOffset.y <= 0;
553
- }
554
- }, []);
555
- const dynamicStyles = useMemo(() => {
556
- return StyleSheet.create({
557
- handle: {
558
- ...styles.handle,
559
- backgroundColor: theme.isDark ? '#444' : '#C7C7CC'
560
- },
561
- sheet: {
562
- ...styles.sheet,
563
- backgroundColor: colors.background,
564
- ...(detached ? styles.sheetDetached : styles.sheetNormal)
565
- },
566
- scrollContent: {
567
- ...styles.scrollContent
568
- // In normal mode, don't add padding here - screens handle their own padding
569
- // The sheet extends behind safe area, and screens add padding as needed
570
- }
571
- });
572
- }, [colors.background, theme.isDark, detached]);
573
- if (!rendered) return null;
574
-
575
- // Default handle render — used when `handleComponent` is not provided.
576
- const renderDefaultHandle = () => /*#__PURE__*/_jsx(View, {
577
- style: dynamicStyles.handle
578
- });
579
- const handleNode = showHandle ? handleComponent ? handleComponent() : renderDefaultHandle() : null;
580
-
581
- // In `manualActivation` mode the handle gets its own gesture detector
582
- // sitting in a dedicated absolutely-positioned hit area at the top of the
583
- // sheet. In legacy mode the handle is rendered inline as a decorative
584
- // overlay (the body pan covers the entire sheet, handle included).
585
- const handleSlot = handleNode && manualActivation && handlePanGesture ? /*#__PURE__*/_jsx(GestureDetector, {
586
- gesture: handlePanGesture,
587
- children: /*#__PURE__*/_jsx(View, {
588
- style: styles.handleHitArea,
589
- accessible: true,
590
- accessibilityRole: "adjustable",
591
- children: handleNode
592
- })
593
- }) : handleNode;
594
-
595
- // Inner content: scrollable wraps in Animated.ScrollView, non-scrollable
596
- // renders children directly. In legacy mode the scrollview is also
597
- // wrapped in the `nativeGesture` detector for scroll/pan coordination.
598
- const scrollViewNode = /*#__PURE__*/_jsx(Animated.ScrollView, {
599
- ref: scrollViewRef,
600
- style: [styles.scrollView, Platform.OS === 'web' && {
601
- scrollbarWidth: 'thin',
602
- scrollbarColor: `${colors.border} transparent`
603
- }],
604
- contentContainerStyle: dynamicStyles.scrollContent,
605
- showsVerticalScrollIndicator: false,
606
- keyboardShouldPersistTaps: "handled",
607
- onScroll: scrollHandler,
608
- scrollEventThrottle: 16,
609
- ...(Platform.OS === 'web' ? {
610
- className: 'bottom-sheet-scrollview'
611
- } : undefined),
612
- onLayout: () => {
613
- if (Platform.OS === 'web') {
614
- createWebScrollbarStyle(colors.border);
615
- }
616
- },
617
- children: children
618
- });
619
- const bodyContent = scrollable ? manualActivation
620
- // In manualActivation mode the scroll view is referenced by the
621
- // pan's simultaneous list directly; no wrapping native gesture.
622
- ? scrollViewNode
623
- // Legacy mode: native gesture wraps the scroll view to coordinate
624
- // with the always-active body pan.
625
- : /*#__PURE__*/_jsx(GestureDetector, {
626
- gesture: nativeGesture,
627
- children: scrollViewNode
628
- }) : /*#__PURE__*/_jsx(View, {
629
- style: styles.nonScrollableContent,
630
- children: children
631
- });
75
+ /**
76
+ * Native shell: RN's `<Modal>` (its own native root window) + the
77
+ * re-established `<KeyboardProvider>` + the sheet's keyboard tracker + a
78
+ * `<GestureHandlerRootView>` (the app-root GHRV does not cross the Modal's
79
+ * native-window boundary, so pan gestures need their own root here).
80
+ */
81
+ function NativeShell({
82
+ visible,
83
+ onRequestClose,
84
+ keyboardHeight,
85
+ children
86
+ }) {
632
87
  return /*#__PURE__*/_jsx(Modal, {
633
- visible: rendered,
88
+ visible: visible,
634
89
  transparent: true,
635
90
  animationType: "none",
636
91
  statusBarTranslucent: true,
637
- onRequestClose: dismiss,
92
+ onRequestClose: onRequestClose,
638
93
  children: /*#__PURE__*/_jsxs(KeyboardProvider, {
639
94
  children: [/*#__PURE__*/_jsx(SheetKeyboardSync, {
640
95
  keyboardHeight: keyboardHeight
641
96
  }), /*#__PURE__*/_jsx(GestureHandlerRootView, {
642
97
  style: styles.rootView,
643
- children: /*#__PURE__*/_jsxs(View, {
644
- style: StyleSheet.absoluteFill,
645
- children: [/*#__PURE__*/_jsx(Animated.View, {
646
- style: [styles.backdrop, backdropStyle],
647
- children: backdropComponent ? backdropComponent({
648
- onPress: handleBackdropPress
649
- }) : /*#__PURE__*/_jsx(Pressable, {
650
- style: styles.backdropTouchable,
651
- onPress: handleBackdropPress,
652
- children: /*#__PURE__*/_jsx(View, {
653
- style: StyleSheet.absoluteFill
654
- })
655
- })
656
- }), /*#__PURE__*/_jsx(GestureDetector, {
657
- gesture: panGesture,
658
- children: /*#__PURE__*/_jsxs(Animated.View, {
659
- style: [dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style],
660
- children: [backgroundComponent?.({
661
- style: styles.background
662
- }), handleSlot, bodyContent]
663
- })
664
- })]
665
- })
98
+ children: children
666
99
  })]
667
100
  })
668
101
  });
669
- });
102
+ }
103
+ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/_jsx(BottomSheetBase, {
104
+ ...props,
105
+ ref: ref,
106
+ Shell: NativeShell
107
+ }));
670
108
  BottomSheet.displayName = 'BottomSheet';
671
109
  const styles = StyleSheet.create({
672
110
  rootView: {
673
111
  flex: 1
674
- },
675
- backdrop: {
676
- // Color is solid black; the dim level is driven by `backdropOpacity` via
677
- // the animated `opacity` style on the wrapping <Animated.View>.
678
- flex: 1,
679
- backgroundColor: '#000'
680
- },
681
- backdropTouchable: {
682
- flex: 1
683
- },
684
- sheet: {
685
- position: 'absolute',
686
- bottom: 0,
687
- overflow: 'hidden',
688
- maxWidth: 800,
689
- alignSelf: 'center',
690
- marginHorizontal: 'auto'
691
- },
692
- sheetDetached: {
693
- left: 16,
694
- right: 16,
695
- borderRadius: 24
696
- },
697
- sheetNormal: {
698
- left: 0,
699
- right: 0,
700
- borderTopLeftRadius: 24,
701
- borderTopRightRadius: 24
702
- },
703
- /** Legacy (non-manualActivation) handle: decorative overlay only. */
704
- handle: {
705
- position: 'absolute',
706
- top: 10,
707
- left: '50%',
708
- marginLeft: -18,
709
- width: 36,
710
- height: 5,
711
- borderRadius: 3,
712
- zIndex: Z_INDEX.sheetHandle
713
- },
714
- /**
715
- * Hit area for the drag handle in `manualActivation` mode. Absolutely
716
- * positioned at the top of the sheet so the area visually "floats" above
717
- * the content — content scrolls up underneath it (no layout offset)
718
- * while the thumb can still grab the full-width 28dp strip to drag.
719
- */
720
- handleHitArea: {
721
- position: 'absolute',
722
- top: 0,
723
- left: 0,
724
- right: 0,
725
- height: 28,
726
- alignItems: 'center',
727
- justifyContent: 'flex-start',
728
- paddingTop: 6,
729
- zIndex: Z_INDEX.sheetHandle
730
- },
731
- background: {
732
- ...StyleSheet.absoluteFill
733
- },
734
- scrollView: {
735
- flex: 1
736
- },
737
- scrollContent: {
738
- flexGrow: 1
739
- },
740
- nonScrollableContent: {
741
- flex: 1
742
112
  }
743
113
  });
744
-
745
- // Create web scrollbar styles dynamically based on theme
746
- const createWebScrollbarStyle = borderColor => {
747
- if (Platform.OS !== 'web' || typeof document === 'undefined') return;
748
- let styleElement = document.getElementById(SCROLLBAR_STYLE_ID);
749
- if (!styleElement) {
750
- styleElement = document.createElement('style');
751
- styleElement.id = SCROLLBAR_STYLE_ID;
752
- document.head.appendChild(styleElement);
753
- }
754
-
755
- // Derive a slightly darker scrollbar hover color from the border color
756
- const scrollbarColor = borderColor;
757
- const scrollbarHoverColor = borderColor.startsWith('hsl') ? borderColor.replace(/\)$/, ' / 0.7)') // add alpha for hover
758
- : '#888';
759
- styleElement.textContent = `
760
- .bottom-sheet-scrollview::-webkit-scrollbar {
761
- width: 6px;
762
- }
763
- .bottom-sheet-scrollview::-webkit-scrollbar-track {
764
- background: transparent;
765
- border-radius: 10px;
766
- }
767
- .bottom-sheet-scrollview::-webkit-scrollbar-thumb {
768
- background: ${scrollbarColor};
769
- border-radius: 10px;
770
- }
771
- .bottom-sheet-scrollview::-webkit-scrollbar-thumb:hover {
772
- background: ${scrollbarHoverColor};
773
- }
774
- `;
775
- };
776
- const SCROLLBAR_STYLE_ID = 'bottom-sheet-scrollbar-style';
777
-
778
- /** Remove the injected scrollbar <style> tag on unmount. */
779
- const removeWebScrollbarStyle = () => {
780
- if (Platform.OS !== 'web' || typeof document === 'undefined') return;
781
- document.getElementById(SCROLLBAR_STYLE_ID)?.remove();
782
- };
783
114
  export default BottomSheet;
784
115
  export { BottomSheet };
785
116
  //# sourceMappingURL=index.js.map