@oxyhq/bloom 0.30.4 → 0.30.5

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