react-native-bottom-sheet-stack 1.9.2 → 1.11.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 (74) hide show
  1. package/README.md +14 -0
  2. package/lib/commonjs/BottomSheetBackdrop.js +15 -17
  3. package/lib/commonjs/BottomSheetBackdrop.js.map +1 -1
  4. package/lib/commonjs/QueueItem.js +32 -28
  5. package/lib/commonjs/QueueItem.js.map +1 -1
  6. package/lib/commonjs/adapters/actions-sheet/ActionsSheetAdapter.js +17 -9
  7. package/lib/commonjs/adapters/actions-sheet/ActionsSheetAdapter.js.map +1 -1
  8. package/lib/commonjs/adapters/gorhom-sheet/GorhomSheetAdapter.js +12 -9
  9. package/lib/commonjs/adapters/gorhom-sheet/GorhomSheetAdapter.js.map +1 -1
  10. package/lib/commonjs/adapters/react-native-modal/ReactNativeModalAdapter.js +14 -8
  11. package/lib/commonjs/adapters/react-native-modal/ReactNativeModalAdapter.js.map +1 -1
  12. package/lib/commonjs/bottomSheetCoordinator.js +103 -0
  13. package/lib/commonjs/bottomSheetCoordinator.js.map +1 -1
  14. package/lib/commonjs/index.js +38 -0
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/onBeforeCloseRegistry.js +42 -0
  17. package/lib/commonjs/onBeforeCloseRegistry.js.map +1 -0
  18. package/lib/commonjs/store/hooks.js +37 -4
  19. package/lib/commonjs/store/hooks.js.map +1 -1
  20. package/lib/commonjs/store/store.js +9 -0
  21. package/lib/commonjs/store/store.js.map +1 -1
  22. package/lib/commonjs/useBottomSheetContext.js +31 -17
  23. package/lib/commonjs/useBottomSheetContext.js.map +1 -1
  24. package/lib/commonjs/useBottomSheetControl.js +47 -33
  25. package/lib/commonjs/useBottomSheetControl.js.map +1 -1
  26. package/lib/commonjs/useBottomSheetManager.js +18 -11
  27. package/lib/commonjs/useBottomSheetManager.js.map +1 -1
  28. package/lib/commonjs/useOnBeforeClose.js +107 -0
  29. package/lib/commonjs/useOnBeforeClose.js.map +1 -0
  30. package/lib/typescript/example/src/screens/HomeScreen.d.ts.map +1 -1
  31. package/lib/typescript/example/src/sheets/CloseInterceptionSheets.d.ts +14 -0
  32. package/lib/typescript/example/src/sheets/CloseInterceptionSheets.d.ts.map +1 -0
  33. package/lib/typescript/example/src/sheets/index.d.ts +1 -0
  34. package/lib/typescript/example/src/sheets/index.d.ts.map +1 -1
  35. package/lib/typescript/src/BottomSheetBackdrop.d.ts.map +1 -1
  36. package/lib/typescript/src/QueueItem.d.ts.map +1 -1
  37. package/lib/typescript/src/adapters/actions-sheet/ActionsSheetAdapter.d.ts.map +1 -1
  38. package/lib/typescript/src/adapters/gorhom-sheet/GorhomSheetAdapter.d.ts.map +1 -1
  39. package/lib/typescript/src/adapters/react-native-modal/ReactNativeModalAdapter.d.ts.map +1 -1
  40. package/lib/typescript/src/bottomSheetCoordinator.d.ts +25 -0
  41. package/lib/typescript/src/bottomSheetCoordinator.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +6 -2
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/onBeforeCloseRegistry.d.ts +26 -0
  45. package/lib/typescript/src/onBeforeCloseRegistry.d.ts.map +1 -0
  46. package/lib/typescript/src/store/hooks.d.ts +3 -0
  47. package/lib/typescript/src/store/hooks.d.ts.map +1 -1
  48. package/lib/typescript/src/store/store.d.ts.map +1 -1
  49. package/lib/typescript/src/store/types.d.ts +9 -0
  50. package/lib/typescript/src/store/types.d.ts.map +1 -1
  51. package/lib/typescript/src/useBottomSheetContext.d.ts +5 -0
  52. package/lib/typescript/src/useBottomSheetContext.d.ts.map +1 -1
  53. package/lib/typescript/src/useBottomSheetControl.d.ts +3 -0
  54. package/lib/typescript/src/useBottomSheetControl.d.ts.map +1 -1
  55. package/lib/typescript/src/useBottomSheetManager.d.ts +7 -0
  56. package/lib/typescript/src/useBottomSheetManager.d.ts.map +1 -1
  57. package/lib/typescript/src/useOnBeforeClose.d.ts +65 -0
  58. package/lib/typescript/src/useOnBeforeClose.d.ts.map +1 -0
  59. package/package.json +1 -1
  60. package/src/BottomSheetBackdrop.tsx +2 -3
  61. package/src/QueueItem.tsx +5 -1
  62. package/src/adapters/actions-sheet/ActionsSheetAdapter.tsx +5 -3
  63. package/src/adapters/gorhom-sheet/GorhomSheetAdapter.tsx +3 -1
  64. package/src/adapters/react-native-modal/ReactNativeModalAdapter.tsx +4 -2
  65. package/src/bottomSheetCoordinator.ts +128 -0
  66. package/src/index.tsx +15 -2
  67. package/src/onBeforeCloseRegistry.ts +44 -0
  68. package/src/store/hooks.ts +12 -0
  69. package/src/store/store.ts +11 -0
  70. package/src/store/types.ts +9 -0
  71. package/src/useBottomSheetContext.ts +11 -1
  72. package/src/useBottomSheetControl.ts +13 -8
  73. package/src/useBottomSheetManager.tsx +16 -8
  74. package/src/useOnBeforeClose.ts +92 -0
@@ -3,6 +3,11 @@ export interface UseBottomSheetContextReturn<TParams> {
3
3
  id: string;
4
4
  params: TParams;
5
5
  close: () => void;
6
+ /**
7
+ * Close the sheet, bypassing any onBeforeClose interceptor.
8
+ * Useful for force-closing from within onBeforeClose confirmation flows.
9
+ */
10
+ forceClose: () => void;
6
11
  /** @deprecated Use `close` instead */
7
12
  closeBottomSheet: () => void;
8
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useBottomSheetContext.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetContext.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,2BAA2B,CAAC,OAAO;IAClD,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,sCAAsC;IACtC,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAC9E,2DAA2D;AAC3D,wBAAgB,qBAAqB,CACnC,CAAC,SAAS,mBAAmB,KAC1B,2BAA2B,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC;AAwB7D;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAAwB,CAAC"}
1
+ {"version":3,"file":"useBottomSheetContext.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetContext.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,2BAA2B,CAAC,OAAO;IAClD,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,sCAAsC;IACtC,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAC9E,2DAA2D;AAC3D,wBAAgB,qBAAqB,CACnC,CAAC,SAAS,mBAAmB,KAC1B,2BAA2B,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC;AA4B7D;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAAwB,CAAC"}
@@ -1,8 +1,10 @@
1
1
  import { type OpenMode } from './bottomSheet.store';
2
2
  import type { BottomSheetPortalId, BottomSheetPortalParams, HasParams } from './portal.types';
3
+ import type { CloseAllOptions } from './useBottomSheetManager';
3
4
  interface BaseOpenOptions<TParams> {
4
5
  mode?: OpenMode;
5
6
  scaleBackground?: boolean;
7
+ backdrop?: boolean;
6
8
  params?: TParams;
7
9
  }
8
10
  type OpenOptions<T extends BottomSheetPortalId> = Omit<BaseOpenOptions<BottomSheetPortalParams<T>>, 'params'> & (HasParams<T> extends true ? {
@@ -14,6 +16,7 @@ type OpenFunction<T extends BottomSheetPortalId> = HasParams<T> extends true ? (
14
16
  export interface UseBottomSheetControlReturn<T extends BottomSheetPortalId> {
15
17
  open: OpenFunction<T>;
16
18
  close: () => void;
19
+ closeAll: (options?: CloseAllOptions) => Promise<void>;
17
20
  updateParams: (params: BottomSheetPortalParams<T>) => void;
18
21
  resetParams: () => void;
19
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useBottomSheetControl.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetControl.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,SAAS,EACV,MAAM,gBAAgB,CAAC;AAGxB,UAAU,eAAe,CAAC,OAAO;IAC/B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,KAAK,WAAW,CAAC,CAAC,SAAS,mBAAmB,IAAI,IAAI,CACpD,eAAe,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAC3C,QAAQ,CACT,GACC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GACtB;IAAE,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAA;CAAE,GACtC;IAAE,MAAM,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,CAAC;AAE/C,KAAK,YAAY,CAAC,CAAC,SAAS,mBAAmB,IAC7C,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GACrB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,GACjC,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAEzC,MAAM,WAAW,2BAA2B,CAAC,CAAC,SAAS,mBAAmB;IACxE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,YAAY,EAAE,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,mBAAmB,EACjE,EAAE,EAAE,CAAC,GACJ,2BAA2B,CAAC,CAAC,CAAC,CAgDhC"}
1
+ {"version":3,"file":"useBottomSheetControl.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetControl.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG9E,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,SAAS,EACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,UAAU,eAAe,CAAC,OAAO;IAC/B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,KAAK,WAAW,CAAC,CAAC,SAAS,mBAAmB,IAAI,IAAI,CACpD,eAAe,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAC3C,QAAQ,CACT,GACC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GACtB;IAAE,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAA;CAAE,GACtC;IAAE,MAAM,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,CAAC;AAE/C,KAAK,YAAY,CAAC,CAAC,SAAS,mBAAmB,IAC7C,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GACrB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,GACjC,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAEzC,MAAM,WAAW,2BAA2B,CAAC,CAAC,SAAS,mBAAmB;IACxE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,YAAY,EAAE,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,mBAAmB,EACjE,EAAE,EAAE,CAAC,GACJ,2BAA2B,CAAC,CAAC,CAAC,CAsDhC"}
@@ -1,13 +1,19 @@
1
1
  import React from 'react';
2
2
  import { type OpenMode } from './bottomSheet.store';
3
+ export interface CloseAllOptions {
4
+ /** Delay in ms between each cascading close animation. Default: 100 */
5
+ stagger?: number;
6
+ }
3
7
  export declare const useBottomSheetManager: () => {
4
8
  open: (content: React.ReactElement, options?: {
5
9
  id?: string;
6
10
  groupId?: string;
7
11
  mode?: OpenMode;
8
12
  scaleBackground?: boolean;
13
+ backdrop?: boolean;
9
14
  }) => string;
10
15
  close: (id: string) => void;
16
+ closeAll: (options?: CloseAllOptions) => Promise<void>;
11
17
  clear: () => void;
12
18
  /** @deprecated Use `open` instead */
13
19
  openBottomSheet: (content: React.ReactElement, options?: {
@@ -15,6 +21,7 @@ export declare const useBottomSheetManager: () => {
15
21
  groupId?: string;
16
22
  mode?: OpenMode;
17
23
  scaleBackground?: boolean;
24
+ backdrop?: boolean;
18
25
  }) => string;
19
26
  /** @deprecated Use `clear` instead */
20
27
  clearAll: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"useBottomSheetManager.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAK7B,eAAO,MAAM,qBAAqB;oBAQrB,KAAK,CAAC,YAAY,YAClB;QACP,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B;gBA2BgB,MAAM;;IAavB,qCAAqC;+BA9C5B,KAAK,CAAC,YAAY,YAClB;QACP,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B;IA0CD,sCAAsC;;CAGzC,CAAC"}
1
+ {"version":3,"file":"useBottomSheetManager.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAM5E,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,qBAAqB;oBAOrB,KAAK,CAAC,YAAY,YAClB;QACP,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;gBA4BgB,MAAM;yBAIG,eAAe;;IAezC,qCAAqC;+BAtD5B,KAAK,CAAC,YAAY,YAClB;QACP,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IAiDD,sCAAsC;;CAGzC,CAAC"}
@@ -0,0 +1,65 @@
1
+ import type { OnBeforeCloseCallback } from './onBeforeCloseRegistry';
2
+ /**
3
+ * Registers an interceptor that is called before the sheet closes.
4
+ *
5
+ * When active, this hook:
6
+ * 1. Sets `preventDismiss` on the sheet so adapters block user-initiated
7
+ * gestures (swipe down, pan-to-close) at the native level.
8
+ * 2. Intercepts all programmatic close paths (backdrop tap, back button,
9
+ * `close()`, `closeAll()`) and calls the callback first.
10
+ *
11
+ * The interceptor receives `onConfirm` and `onCancel` callbacks. Call these
12
+ * when the user makes a decision. This works seamlessly with `Alert.alert`:
13
+ *
14
+ * ```tsx
15
+ * useOnBeforeClose(({ onConfirm, onCancel }) => {
16
+ * if (dirty) {
17
+ * Alert.alert('Discard changes?', '', [
18
+ * { text: 'Cancel', onPress: onCancel },
19
+ * { text: 'Discard', onPress: onConfirm },
20
+ * ]);
21
+ * } else {
22
+ * onConfirm(); // Allow close immediately
23
+ * }
24
+ * });
25
+ * ```
26
+ *
27
+ * For backward compatibility, you can still return `boolean` or `Promise<boolean>`:
28
+ * - Return `false` (or resolve to `false`) to prevent closing
29
+ * - Return `true` (or resolve to `true`) to allow closing
30
+ *
31
+ * Use `forceClose()` from `useBottomSheetContext` to bypass the interceptor entirely.
32
+ *
33
+ * Must be used inside a sheet component (within BottomSheetContext).
34
+ *
35
+ * @example Callback pattern (recommended)
36
+ * ```tsx
37
+ * function MySheet() {
38
+ * const [dirty, setDirty] = useState(false);
39
+ *
40
+ * useOnBeforeClose(({ onConfirm, onCancel }) => {
41
+ * if (dirty) {
42
+ * Alert.alert('Discard changes?', '', [
43
+ * { text: 'Cancel', style: 'cancel', onPress: onCancel },
44
+ * { text: 'Discard', onPress: onConfirm },
45
+ * ]);
46
+ * } else {
47
+ * onConfirm();
48
+ * }
49
+ * });
50
+ * }
51
+ * ```
52
+ *
53
+ * @example Boolean return (backward compatible)
54
+ * ```tsx
55
+ * function MySheet() {
56
+ * const [dirty, setDirty] = useState(false);
57
+ *
58
+ * useOnBeforeClose(() => {
59
+ * return !dirty; // false blocks, true allows
60
+ * });
61
+ * }
62
+ * ```
63
+ */
64
+ export declare function useOnBeforeClose(callback: OnBeforeCloseCallback): void;
65
+ //# sourceMappingURL=useOnBeforeClose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOnBeforeClose.d.ts","sourceRoot":"","sources":["../../../src/useOnBeforeClose.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAIrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAqBtE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-bottom-sheet-stack",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "description": "Bottom Sheet Stack Manager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "lib/commonjs/index.js",
@@ -6,7 +6,7 @@ import Animated, {
6
6
  useAnimatedStyle,
7
7
  } from 'react-native-reanimated';
8
8
  import { getAnimatedIndex } from './animatedRegistry';
9
- import { useStartClosing } from './store';
9
+ import { requestClose } from './bottomSheetCoordinator';
10
10
 
11
11
  interface BottomSheetBackdropProps {
12
12
  sheetId: string;
@@ -14,7 +14,6 @@ interface BottomSheetBackdropProps {
14
14
 
15
15
  export function BottomSheetBackdrop({ sheetId }: BottomSheetBackdropProps) {
16
16
  const animatedIndex = getAnimatedIndex(sheetId);
17
- const startClosing = useStartClosing();
18
17
 
19
18
  if (!animatedIndex) {
20
19
  throw new Error('animatedIndex must be defined in BottomSheetBackdrop');
@@ -46,7 +45,7 @@ export function BottomSheetBackdrop({ sheetId }: BottomSheetBackdropProps) {
46
45
  return (
47
46
  <Pressable
48
47
  style={StyleSheet.absoluteFillObject}
49
- onPress={() => startClosing(sheetId)}
48
+ onPress={() => requestClose(sheetId)}
50
49
  >
51
50
  <Animated.View
52
51
  style={[StyleSheet.absoluteFillObject, animatedStyle, styles.backdrop]}
package/src/QueueItem.tsx CHANGED
@@ -7,12 +7,14 @@ import { PortalHost } from 'react-native-teleport';
7
7
  import { cleanupAnimatedIndex, getAnimatedIndex } from './animatedRegistry';
8
8
  import { BottomSheetContext } from './BottomSheet.context';
9
9
  import {
10
+ useSheetBackdrop,
10
11
  useSheetContent,
11
12
  useSheetKeepMounted,
12
13
  useSheetPortalSession,
13
14
  useSheetUsePortal,
14
15
  } from './bottomSheet.store';
15
16
  import { BottomSheetBackdrop } from './BottomSheetBackdrop';
17
+ import { removeOnBeforeClose } from './onBeforeCloseRegistry';
16
18
  import { cleanupSheetRef } from './refsMap';
17
19
  import { useSheetScaleAnimatedStyle } from './useScaleAnimation';
18
20
 
@@ -31,6 +33,7 @@ export const QueueItem = memo(function QueueItem({
31
33
  const usePortal = useSheetUsePortal(id);
32
34
  const keepMounted = useSheetKeepMounted(id);
33
35
  const portalSession = useSheetPortalSession(id);
36
+ const backdrop = useSheetBackdrop(id);
34
37
 
35
38
  const { width, height } = useSafeAreaFrame();
36
39
 
@@ -40,6 +43,7 @@ export const QueueItem = memo(function QueueItem({
40
43
  return () => {
41
44
  cleanupSheetRef(id);
42
45
  cleanupAnimatedIndex(id);
46
+ removeOnBeforeClose(id);
43
47
  };
44
48
  }, [id, keepMounted]);
45
49
 
@@ -54,7 +58,7 @@ export const QueueItem = memo(function QueueItem({
54
58
 
55
59
  return (
56
60
  <>
57
- {isActive && (
61
+ {isActive && backdrop !== false && (
58
62
  <View
59
63
  style={[StyleSheet.absoluteFillObject, { zIndex: backdropZIndex }]}
60
64
  pointerEvents="box-none"
@@ -1,6 +1,7 @@
1
1
  import React, { useImperativeHandle, useRef } from 'react';
2
2
 
3
3
  import type { SheetAdapterRef } from '../../adapter.types';
4
+ import { useSheetPreventDismiss } from '../../bottomSheet.store';
4
5
  import { createSheetEventHandlers } from '../../bottomSheetCoordinator';
5
6
  import { useAdapterRef } from '../../useAdapterRef';
6
7
  import { useAnimatedIndex } from '../../useAnimatedIndex';
@@ -35,6 +36,7 @@ export const ActionsSheetAdapter = React.forwardRef<
35
36
  const { id } = useBottomSheetContext();
36
37
  const ref = useAdapterRef(forwardedRef);
37
38
  const animatedIndex = useAnimatedIndex();
39
+ const preventDismiss = useSheetPreventDismiss(id);
38
40
 
39
41
  const actionSheetRef = useRef<any>(null);
40
42
 
@@ -63,9 +65,9 @@ export const ActionsSheetAdapter = React.forwardRef<
63
65
  return (
64
66
  <ActionSheet
65
67
  // Adapter defaults (overridable via spread)
66
- gestureEnabled
67
- closeOnTouchBackdrop
68
- closeOnPressBack
68
+ gestureEnabled={!preventDismiss}
69
+ closeOnTouchBackdrop={!preventDismiss}
70
+ closeOnPressBack={!preventDismiss}
69
71
  keyboardHandlerEnabled
70
72
  {...sheetProps}
71
73
  // Managed by adapter (not overridable)
@@ -7,6 +7,7 @@ import React, { useImperativeHandle, useRef } from 'react';
7
7
  import { useAnimatedReaction } from 'react-native-reanimated';
8
8
 
9
9
  import type { SheetAdapterRef } from '../../adapter.types';
10
+ import { useSheetPreventDismiss } from '../../bottomSheet.store';
10
11
  import { createSheetEventHandlers } from '../../bottomSheetCoordinator';
11
12
  import { useBottomSheetDefaultIndex } from '../../BottomSheetDefaultIndex.context';
12
13
  import { useAdapterRef } from '../../useAdapterRef';
@@ -39,6 +40,7 @@ export const GorhomSheetAdapter = React.forwardRef<
39
40
  const ref = useAdapterRef(forwardedRef);
40
41
  const contextAnimatedIndex = useAnimatedIndex();
41
42
  const defaultIndex = useBottomSheetDefaultIndex();
43
+ const preventDismiss = useSheetPreventDismiss(id);
42
44
 
43
45
  const gorhomRef = useRef<BottomSheetMethods | null>(null);
44
46
 
@@ -110,7 +112,7 @@ export const GorhomSheetAdapter = React.forwardRef<
110
112
  onClose={wrappedOnClose}
111
113
  onAnimate={wrappedOnAnimate}
112
114
  backdropComponent={backdropComponent}
113
- enablePanDownToClose={enablePanDownToClose}
115
+ enablePanDownToClose={preventDismiss ? false : enablePanDownToClose}
114
116
  >
115
117
  {children}
116
118
  </BottomSheetOriginal>
@@ -1,6 +1,7 @@
1
1
  import React, { useImperativeHandle, useState } from 'react';
2
2
 
3
3
  import type { SheetAdapterRef } from '../../adapter.types';
4
+ import { useSheetPreventDismiss } from '../../bottomSheet.store';
4
5
  import { createSheetEventHandlers } from '../../bottomSheetCoordinator';
5
6
  import { useAdapterRef } from '../../useAdapterRef';
6
7
  import { useAnimatedIndex } from '../../useAnimatedIndex';
@@ -34,6 +35,7 @@ export const ReactNativeModalAdapter = React.forwardRef<
34
35
  const { id } = useBottomSheetContext();
35
36
  const ref = useAdapterRef(forwardedRef);
36
37
  const animatedIndex = useAnimatedIndex();
38
+ const preventDismiss = useSheetPreventDismiss(id);
37
39
  const [isVisible, setIsVisible] = useState(false);
38
40
 
39
41
  const { handleDismiss, handleOpened, handleClosed } =
@@ -57,7 +59,7 @@ export const ReactNativeModalAdapter = React.forwardRef<
57
59
  return (
58
60
  <RNModal
59
61
  // Adapter defaults (overridable via spread)
60
- swipeDirection="down"
62
+ swipeDirection={preventDismiss ? undefined : 'down'}
61
63
  useNativeDriver
62
64
  hideModalContentWhileAnimating
63
65
  {...modalProps}
@@ -71,7 +73,7 @@ export const ReactNativeModalAdapter = React.forwardRef<
71
73
  onModalShow={handleOpened}
72
74
  onModalHide={handleClosed}
73
75
  onBackButtonPress={handleDismiss}
74
- onSwipeComplete={handleDismiss}
76
+ onSwipeComplete={preventDismiss ? undefined : handleDismiss}
75
77
  >
76
78
  {children}
77
79
  </RNModal>
@@ -1,5 +1,6 @@
1
1
  import type { SheetAdapterEvents } from './adapter.types';
2
2
  import { useBottomSheetStore } from './bottomSheet.store';
3
+ import { getOnBeforeClose } from './onBeforeCloseRegistry';
3
4
  import { getSheetRef } from './refsMap';
4
5
 
5
6
  /**
@@ -42,6 +43,126 @@ export function initBottomSheetCoordinator(groupId: string) {
42
43
  );
43
44
  }
44
45
 
46
+ /**
47
+ * Attempts to close a sheet, respecting the onBeforeClose interceptor.
48
+ *
49
+ * If an onBeforeClose callback is registered for the sheet and it returns
50
+ * `false` (or resolves to `false`), the close is cancelled.
51
+ *
52
+ * @returns `true` if the close proceeded, `false` if it was intercepted.
53
+ */
54
+ export async function requestClose(sheetId: string): Promise<boolean> {
55
+ const state = useBottomSheetStore.getState();
56
+ const currentStatus = state.sheetsById[sheetId]?.status;
57
+
58
+ // Don't run interceptor if sheet is already closing
59
+ // This prevents duplicate interceptor calls during close animations
60
+ if (currentStatus === 'closing') {
61
+ return false;
62
+ }
63
+
64
+ const interceptor = getOnBeforeClose(sheetId);
65
+
66
+ if (interceptor) {
67
+ try {
68
+ const allowed = await new Promise<boolean>((resolve) => {
69
+ const result = interceptor({
70
+ onConfirm: () => resolve(true),
71
+ onCancel: () => resolve(false),
72
+ });
73
+
74
+ if (result) {
75
+ if (typeof result === 'boolean') {
76
+ resolve(result);
77
+ } else if (
78
+ result &&
79
+ typeof result === 'object' &&
80
+ 'then' in result &&
81
+ typeof result.then === 'function'
82
+ ) {
83
+ // It's a Promise
84
+ result.then(resolve);
85
+ }
86
+ }
87
+ });
88
+
89
+ if (!allowed) {
90
+ return false;
91
+ }
92
+ } catch (error) {
93
+ // If the interceptor throws, cancel the close for safety
94
+ if (__DEV__) {
95
+ console.warn(
96
+ `[BottomSheet] onBeforeClose interceptor threw an error for sheet "${sheetId}". ` +
97
+ 'Close cancelled for safety. Fix the interceptor to avoid this warning.',
98
+ error
99
+ );
100
+ }
101
+ return false;
102
+ }
103
+ }
104
+
105
+ if (currentStatus === 'open' || currentStatus === 'opening') {
106
+ state.startClosing(sheetId);
107
+ }
108
+
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * Default stagger delay between cascading close animations (ms).
114
+ */
115
+ const DEFAULT_STAGGER_MS = 100;
116
+
117
+ /**
118
+ * Closes all sheets in a group from top to bottom with cascading animation.
119
+ *
120
+ * Each sheet is closed with a staggered delay so the user sees them
121
+ * peel off one-by-one (similar to `popToRoot` in React Navigation).
122
+ *
123
+ * If a sheet has an `onBeforeClose` interceptor that rejects, the cascade
124
+ * stops at that sheet — sheets below it remain open.
125
+ *
126
+ * @param groupId - The manager group to close sheets in.
127
+ * @param options.stagger - Delay in ms between each close (default: 100).
128
+ * @returns A promise that resolves when the cascade finishes (or is stopped).
129
+ */
130
+ export async function closeAllAnimated(
131
+ groupId: string,
132
+ options?: { stagger?: number }
133
+ ): Promise<void> {
134
+ const stagger = options?.stagger ?? DEFAULT_STAGGER_MS;
135
+
136
+ const state = useBottomSheetStore.getState();
137
+ const groupSheetIds = state.stackOrder.filter(
138
+ (id) => state.sheetsById[id]?.groupId === groupId
139
+ );
140
+
141
+ // Close from top to bottom (reverse order)
142
+ const reversed = [...groupSheetIds].reverse();
143
+
144
+ for (const sheetId of reversed) {
145
+ const currentState = useBottomSheetStore.getState();
146
+ const sheet = currentState.sheetsById[sheetId];
147
+
148
+ // Skip sheets that are already closing or hidden
149
+ if (!sheet || sheet.status === 'closing' || sheet.status === 'hidden') {
150
+ continue;
151
+ }
152
+
153
+ const closed = await requestClose(sheetId);
154
+
155
+ if (!closed) {
156
+ // Interceptor blocked — stop the cascade
157
+ break;
158
+ }
159
+
160
+ if (stagger > 0 && reversed.indexOf(sheetId) < reversed.length - 1) {
161
+ await new Promise<void>((resolve) => setTimeout(resolve, stagger));
162
+ }
163
+ }
164
+ }
165
+
45
166
  /**
46
167
  * Creates event handlers that adapters call to sync UI state back to the store.
47
168
  * Direction: Adapter Events → Store
@@ -53,6 +174,13 @@ export function initBottomSheetCoordinator(groupId: string) {
53
174
  */
54
175
  export function createSheetEventHandlers(sheetId: string): SheetAdapterEvents {
55
176
  const handleDismiss = () => {
177
+ const interceptor = getOnBeforeClose(sheetId);
178
+
179
+ if (interceptor) {
180
+ requestClose(sheetId);
181
+ return;
182
+ }
183
+
56
184
  const state = useBottomSheetStore.getState();
57
185
  const currentStatus = state.sheetsById[sheetId]?.status;
58
186
 
package/src/index.tsx CHANGED
@@ -20,14 +20,21 @@ export type {
20
20
  } from './adapter.types';
21
21
 
22
22
  // Adapter utilities (for custom adapter authors)
23
- export { createSheetEventHandlers } from './bottomSheetCoordinator';
23
+ export {
24
+ createSheetEventHandlers,
25
+ requestClose,
26
+ closeAllAnimated,
27
+ } from './bottomSheetCoordinator';
24
28
  export { useAdapterRef } from './useAdapterRef';
25
29
  export { useAnimatedIndex } from './useAnimatedIndex';
26
30
  export { useBackHandler } from './useBackHandler';
27
31
  export { getAnimatedIndex, setAnimatedIndexValue } from './animatedRegistry';
28
32
 
29
33
  // Hooks
30
- export { useBottomSheetManager } from './useBottomSheetManager';
34
+ export {
35
+ useBottomSheetManager,
36
+ type CloseAllOptions,
37
+ } from './useBottomSheetManager';
31
38
  export {
32
39
  useBottomSheetControl,
33
40
  type UseBottomSheetControlReturn,
@@ -41,6 +48,7 @@ export {
41
48
  useBottomSheetStatus,
42
49
  type UseBottomSheetStatusReturn,
43
50
  } from './useBottomSheetStatus';
51
+ export { useOnBeforeClose } from './useOnBeforeClose';
44
52
 
45
53
  // Types
46
54
  export type { ScaleConfig, ScaleAnimationConfig } from './useScaleAnimation';
@@ -57,6 +65,10 @@ export type {
57
65
 
58
66
  export { useBottomSheetStore } from './bottomSheet.store';
59
67
 
68
+ // onBeforeClose registry
69
+ export type { OnBeforeCloseCallback } from './onBeforeCloseRegistry';
70
+ export { setOnBeforeClose, removeOnBeforeClose } from './onBeforeCloseRegistry';
71
+
60
72
  // Testing utilities (internal use)
61
73
  export { __resetSheetRefs } from './refsMap';
62
74
  export {
@@ -64,3 +76,4 @@ export {
64
76
  __getAllAnimatedIndexes,
65
77
  } from './animatedRegistry';
66
78
  export { __resetPortalSessions } from './portalSessionRegistry';
79
+ export { __resetOnBeforeClose } from './onBeforeCloseRegistry';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Registry for onBeforeClose interceptors.
3
+ *
4
+ * Callbacks are keyed by sheet ID. When a sheet is about to close,
5
+ * the coordinator checks this registry and calls the interceptor.
6
+ *
7
+ * The interceptor receives `onConfirm` and `onCancel` callbacks that
8
+ * should be called when the user makes a decision. Alternatively, it can
9
+ * return `boolean` or `Promise<boolean>` for backward compatibility.
10
+ *
11
+ * If the interceptor returns `false` (or resolves to `false`), or if
12
+ * `onCancel()` is called, the close is cancelled.
13
+ */
14
+ export type OnBeforeCloseCallback = (context: {
15
+ onConfirm: () => void;
16
+ onCancel: () => void;
17
+ }) => void | boolean | Promise<boolean>;
18
+
19
+ const onBeforeCloseMap = new Map<string, OnBeforeCloseCallback>();
20
+
21
+ export function getOnBeforeClose(
22
+ sheetId: string
23
+ ): OnBeforeCloseCallback | undefined {
24
+ return onBeforeCloseMap.get(sheetId);
25
+ }
26
+
27
+ export function setOnBeforeClose(
28
+ sheetId: string,
29
+ callback: OnBeforeCloseCallback
30
+ ): void {
31
+ onBeforeCloseMap.set(sheetId, callback);
32
+ }
33
+
34
+ export function removeOnBeforeClose(sheetId: string): void {
35
+ onBeforeCloseMap.delete(sheetId);
36
+ }
37
+
38
+ /**
39
+ * Reset all interceptors. Useful for testing.
40
+ * @internal
41
+ */
42
+ export function __resetOnBeforeClose(): void {
43
+ onBeforeCloseMap.clear();
44
+ }
@@ -20,8 +20,17 @@ export const useSheetUsePortal = (id: string) =>
20
20
 
21
21
  export const useSheetKeepMounted = (id: string) =>
22
22
  useBottomSheetStore((state) => state.sheetsById[id]?.keepMounted, shallow);
23
+
24
+ export const useSheetBackdrop = (id: string) =>
25
+ useBottomSheetStore((state) => state.sheetsById[id]?.backdrop, shallow);
26
+
23
27
  export const useSheetPortalSession = (id: string) =>
24
28
  useBottomSheetStore((state) => state.sheetsById[id]?.portalSession, shallow);
29
+ export const useSheetPreventDismiss = (id: string) =>
30
+ useBottomSheetStore(
31
+ (state) => state.sheetsById[id]?.preventDismiss ?? false,
32
+ shallow
33
+ );
25
34
 
26
35
  export const useSheetExists = (id: string) =>
27
36
  useBottomSheetStore((state) => !!state.sheetsById[id], shallow);
@@ -70,6 +79,9 @@ export const useUpdateParams = () =>
70
79
  export const useClearGroup = () =>
71
80
  useBottomSheetStore((state) => state.clearGroup);
72
81
 
82
+ export const useSetPreventDismiss = () =>
83
+ useBottomSheetStore((state) => state.setPreventDismiss);
84
+
73
85
  export const useMount = () => useBottomSheetStore((state) => state.mount);
74
86
 
75
87
  export const useUnmount = () => useBottomSheetStore((state) => state.unmount);
@@ -54,6 +54,7 @@ export const useBottomSheetStore = create(
54
54
  status: 'opening',
55
55
  scaleBackground:
56
56
  sheet.scaleBackground ?? existingSheet.scaleBackground,
57
+ backdrop: sheet.backdrop ?? existingSheet.backdrop,
57
58
  params: sheet.params ?? existingSheet.params,
58
59
  portalSession: existingSheet.keepMounted
59
60
  ? existingSheet.portalSession
@@ -130,6 +131,16 @@ export const useBottomSheetStore = create(
130
131
  return { sheetsById: updateSheet(state.sheetsById, id, { params }) };
131
132
  }),
132
133
 
134
+ setPreventDismiss: (id, prevent) =>
135
+ set((state) => {
136
+ if (!state.sheetsById[id]) return state;
137
+ return {
138
+ sheetsById: updateSheet(state.sheetsById, id, {
139
+ preventDismiss: prevent,
140
+ }),
141
+ };
142
+ }),
143
+
133
144
  clearGroup: (groupId) =>
134
145
  set((state) => {
135
146
  const idsToRemove = new Set(
@@ -9,6 +9,7 @@ export interface BottomSheetState {
9
9
  content?: ReactNode;
10
10
  status: BottomSheetStatus;
11
11
  scaleBackground?: boolean;
12
+ backdrop?: boolean;
12
13
  usePortal?: boolean;
13
14
  params?: Record<string, unknown>;
14
15
  keepMounted?: boolean;
@@ -18,6 +19,13 @@ export interface BottomSheetState {
18
19
  * react-native-teleport connection issues after replace flows.
19
20
  */
20
21
  portalSession?: number;
22
+ /**
23
+ * When true, the adapter should block user-initiated dismiss gestures
24
+ * (swipe down, backdrop tap). Set by `useOnBeforeClose` to ensure the
25
+ * interceptor runs before closing. Programmatic close via `forceClose()`
26
+ * bypasses this.
27
+ */
28
+ preventDismiss?: boolean;
21
29
  }
22
30
 
23
31
  export type TriggerState = Omit<BottomSheetState, 'status'>;
@@ -33,6 +41,7 @@ export interface BottomSheetStoreActions {
33
41
  startClosing(id: string): void;
34
42
  finishClosing(id: string): void;
35
43
  updateParams(id: string, params: Record<string, unknown> | undefined): void;
44
+ setPreventDismiss(id: string, prevent: boolean): void;
36
45
  clearGroup(groupId: string): void;
37
46
  clearAll(): void;
38
47
  mount(sheet: Omit<BottomSheetState, 'status'>): void;