react-native-bottom-sheet-stack 1.9.2 → 1.10.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.
- package/README.md +14 -0
- package/lib/commonjs/BottomSheetBackdrop.js +15 -17
- package/lib/commonjs/BottomSheetBackdrop.js.map +1 -1
- package/lib/commonjs/QueueItem.js +2 -0
- package/lib/commonjs/QueueItem.js.map +1 -1
- package/lib/commonjs/adapters/actions-sheet/ActionsSheetAdapter.js +17 -9
- package/lib/commonjs/adapters/actions-sheet/ActionsSheetAdapter.js.map +1 -1
- package/lib/commonjs/adapters/gorhom-sheet/GorhomSheetAdapter.js +12 -9
- package/lib/commonjs/adapters/gorhom-sheet/GorhomSheetAdapter.js.map +1 -1
- package/lib/commonjs/adapters/react-native-modal/ReactNativeModalAdapter.js +14 -8
- package/lib/commonjs/adapters/react-native-modal/ReactNativeModalAdapter.js.map +1 -1
- package/lib/commonjs/bottomSheetCoordinator.js +103 -0
- package/lib/commonjs/bottomSheetCoordinator.js.map +1 -1
- package/lib/commonjs/index.js +38 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/onBeforeCloseRegistry.js +42 -0
- package/lib/commonjs/onBeforeCloseRegistry.js.map +1 -0
- package/lib/commonjs/store/hooks.js +24 -4
- package/lib/commonjs/store/hooks.js.map +1 -1
- package/lib/commonjs/store/store.js +8 -0
- package/lib/commonjs/store/store.js.map +1 -1
- package/lib/commonjs/useBottomSheetContext.js +31 -17
- package/lib/commonjs/useBottomSheetContext.js.map +1 -1
- package/lib/commonjs/useBottomSheetControl.js +46 -33
- package/lib/commonjs/useBottomSheetControl.js.map +1 -1
- package/lib/commonjs/useBottomSheetManager.js +16 -10
- package/lib/commonjs/useBottomSheetManager.js.map +1 -1
- package/lib/commonjs/useOnBeforeClose.js +107 -0
- package/lib/commonjs/useOnBeforeClose.js.map +1 -0
- package/lib/typescript/example/src/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/example/src/sheets/CloseInterceptionSheets.d.ts +14 -0
- package/lib/typescript/example/src/sheets/CloseInterceptionSheets.d.ts.map +1 -0
- package/lib/typescript/example/src/sheets/index.d.ts +1 -0
- package/lib/typescript/example/src/sheets/index.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetBackdrop.d.ts.map +1 -1
- package/lib/typescript/src/QueueItem.d.ts.map +1 -1
- package/lib/typescript/src/adapters/actions-sheet/ActionsSheetAdapter.d.ts.map +1 -1
- package/lib/typescript/src/adapters/gorhom-sheet/GorhomSheetAdapter.d.ts.map +1 -1
- package/lib/typescript/src/adapters/react-native-modal/ReactNativeModalAdapter.d.ts.map +1 -1
- package/lib/typescript/src/bottomSheetCoordinator.d.ts +25 -0
- package/lib/typescript/src/bottomSheetCoordinator.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/onBeforeCloseRegistry.d.ts +26 -0
- package/lib/typescript/src/onBeforeCloseRegistry.d.ts.map +1 -0
- package/lib/typescript/src/store/hooks.d.ts +2 -0
- package/lib/typescript/src/store/hooks.d.ts.map +1 -1
- package/lib/typescript/src/store/store.d.ts.map +1 -1
- package/lib/typescript/src/store/types.d.ts +8 -0
- package/lib/typescript/src/store/types.d.ts.map +1 -1
- package/lib/typescript/src/useBottomSheetContext.d.ts +5 -0
- package/lib/typescript/src/useBottomSheetContext.d.ts.map +1 -1
- package/lib/typescript/src/useBottomSheetControl.d.ts +2 -0
- package/lib/typescript/src/useBottomSheetControl.d.ts.map +1 -1
- package/lib/typescript/src/useBottomSheetManager.d.ts +5 -0
- package/lib/typescript/src/useBottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/src/useOnBeforeClose.d.ts +65 -0
- package/lib/typescript/src/useOnBeforeClose.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/BottomSheetBackdrop.tsx +2 -3
- package/src/QueueItem.tsx +2 -0
- package/src/adapters/actions-sheet/ActionsSheetAdapter.tsx +5 -3
- package/src/adapters/gorhom-sheet/GorhomSheetAdapter.tsx +3 -1
- package/src/adapters/react-native-modal/ReactNativeModalAdapter.tsx +4 -2
- package/src/bottomSheetCoordinator.ts +128 -0
- package/src/index.tsx +15 -2
- package/src/onBeforeCloseRegistry.ts +44 -0
- package/src/store/hooks.ts +8 -0
- package/src/store/store.ts +10 -0
- package/src/store/types.ts +8 -0
- package/src/useBottomSheetContext.ts +11 -1
- package/src/useBottomSheetControl.ts +11 -8
- package/src/useBottomSheetManager.tsx +14 -8
- package/src/useOnBeforeClose.ts +92 -0
|
@@ -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=
|
|
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 {
|
|
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 {
|
|
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
|
+
}
|
package/src/store/hooks.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const useSheetKeepMounted = (id: string) =>
|
|
|
22
22
|
useBottomSheetStore((state) => state.sheetsById[id]?.keepMounted, shallow);
|
|
23
23
|
export const useSheetPortalSession = (id: string) =>
|
|
24
24
|
useBottomSheetStore((state) => state.sheetsById[id]?.portalSession, shallow);
|
|
25
|
+
export const useSheetPreventDismiss = (id: string) =>
|
|
26
|
+
useBottomSheetStore(
|
|
27
|
+
(state) => state.sheetsById[id]?.preventDismiss ?? false,
|
|
28
|
+
shallow
|
|
29
|
+
);
|
|
25
30
|
|
|
26
31
|
export const useSheetExists = (id: string) =>
|
|
27
32
|
useBottomSheetStore((state) => !!state.sheetsById[id], shallow);
|
|
@@ -70,6 +75,9 @@ export const useUpdateParams = () =>
|
|
|
70
75
|
export const useClearGroup = () =>
|
|
71
76
|
useBottomSheetStore((state) => state.clearGroup);
|
|
72
77
|
|
|
78
|
+
export const useSetPreventDismiss = () =>
|
|
79
|
+
useBottomSheetStore((state) => state.setPreventDismiss);
|
|
80
|
+
|
|
73
81
|
export const useMount = () => useBottomSheetStore((state) => state.mount);
|
|
74
82
|
|
|
75
83
|
export const useUnmount = () => useBottomSheetStore((state) => state.unmount);
|
package/src/store/store.ts
CHANGED
|
@@ -130,6 +130,16 @@ export const useBottomSheetStore = create(
|
|
|
130
130
|
return { sheetsById: updateSheet(state.sheetsById, id, { params }) };
|
|
131
131
|
}),
|
|
132
132
|
|
|
133
|
+
setPreventDismiss: (id, prevent) =>
|
|
134
|
+
set((state) => {
|
|
135
|
+
if (!state.sheetsById[id]) return state;
|
|
136
|
+
return {
|
|
137
|
+
sheetsById: updateSheet(state.sheetsById, id, {
|
|
138
|
+
preventDismiss: prevent,
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
|
|
133
143
|
clearGroup: (groupId) =>
|
|
134
144
|
set((state) => {
|
|
135
145
|
const idsToRemove = new Set(
|
package/src/store/types.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface BottomSheetState {
|
|
|
18
18
|
* react-native-teleport connection issues after replace flows.
|
|
19
19
|
*/
|
|
20
20
|
portalSession?: number;
|
|
21
|
+
/**
|
|
22
|
+
* When true, the adapter should block user-initiated dismiss gestures
|
|
23
|
+
* (swipe down, backdrop tap). Set by `useOnBeforeClose` to ensure the
|
|
24
|
+
* interceptor runs before closing. Programmatic close via `forceClose()`
|
|
25
|
+
* bypasses this.
|
|
26
|
+
*/
|
|
27
|
+
preventDismiss?: boolean;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export type TriggerState = Omit<BottomSheetState, 'status'>;
|
|
@@ -33,6 +40,7 @@ export interface BottomSheetStoreActions {
|
|
|
33
40
|
startClosing(id: string): void;
|
|
34
41
|
finishClosing(id: string): void;
|
|
35
42
|
updateParams(id: string, params: Record<string, unknown> | undefined): void;
|
|
43
|
+
setPreventDismiss(id: string, prevent: boolean): void;
|
|
36
44
|
clearGroup(groupId: string): void;
|
|
37
45
|
clearAll(): void;
|
|
38
46
|
mount(sheet: Omit<BottomSheetState, 'status'>): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMaybeBottomSheetContext } from './BottomSheet.context';
|
|
2
2
|
import { useSheetParams, useStartClosing } from './bottomSheet.store';
|
|
3
|
+
import { requestClose } from './bottomSheetCoordinator';
|
|
3
4
|
import type {
|
|
4
5
|
BottomSheetPortalId,
|
|
5
6
|
BottomSheetPortalParams,
|
|
@@ -9,6 +10,11 @@ export interface UseBottomSheetContextReturn<TParams> {
|
|
|
9
10
|
id: string;
|
|
10
11
|
params: TParams;
|
|
11
12
|
close: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* Close the sheet, bypassing any onBeforeClose interceptor.
|
|
15
|
+
* Useful for force-closing from within onBeforeClose confirmation flows.
|
|
16
|
+
*/
|
|
17
|
+
forceClose: () => void;
|
|
12
18
|
/** @deprecated Use `close` instead */
|
|
13
19
|
closeBottomSheet: () => void;
|
|
14
20
|
}
|
|
@@ -32,12 +38,16 @@ export function useBottomSheetContext<
|
|
|
32
38
|
);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
const close = () =>
|
|
41
|
+
const close = () => {
|
|
42
|
+
requestClose(context.id);
|
|
43
|
+
};
|
|
44
|
+
const forceClose = () => startClosing(context.id);
|
|
36
45
|
|
|
37
46
|
return {
|
|
38
47
|
id: context.id,
|
|
39
48
|
params: params as BottomSheetPortalParams<T>,
|
|
40
49
|
close,
|
|
50
|
+
forceClose,
|
|
41
51
|
closeBottomSheet: close,
|
|
42
52
|
};
|
|
43
53
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { SheetAdapterRef } from './adapter.types';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
useOpen,
|
|
6
|
-
useStartClosing,
|
|
7
|
-
useUpdateParams,
|
|
8
|
-
type OpenMode,
|
|
9
|
-
} from './bottomSheet.store';
|
|
4
|
+
import { useOpen, useUpdateParams, type OpenMode } from './bottomSheet.store';
|
|
10
5
|
import { useMaybeBottomSheetManagerContext } from './BottomSheetManager.provider';
|
|
6
|
+
import { closeAllAnimated, requestClose } from './bottomSheetCoordinator';
|
|
11
7
|
import type {
|
|
12
8
|
BottomSheetPortalId,
|
|
13
9
|
BottomSheetPortalParams,
|
|
14
10
|
HasParams,
|
|
15
11
|
} from './portal.types';
|
|
16
12
|
import { getSheetRef, setSheetRef } from './refsMap';
|
|
13
|
+
import type { CloseAllOptions } from './useBottomSheetManager';
|
|
17
14
|
|
|
18
15
|
interface BaseOpenOptions<TParams> {
|
|
19
16
|
mode?: OpenMode;
|
|
@@ -37,6 +34,7 @@ type OpenFunction<T extends BottomSheetPortalId> =
|
|
|
37
34
|
export interface UseBottomSheetControlReturn<T extends BottomSheetPortalId> {
|
|
38
35
|
open: OpenFunction<T>;
|
|
39
36
|
close: () => void;
|
|
37
|
+
closeAll: (options?: CloseAllOptions) => Promise<void>;
|
|
40
38
|
updateParams: (params: BottomSheetPortalParams<T>) => void;
|
|
41
39
|
resetParams: () => void;
|
|
42
40
|
}
|
|
@@ -47,7 +45,6 @@ export function useBottomSheetControl<T extends BottomSheetPortalId>(
|
|
|
47
45
|
const bottomSheetManagerContext = useMaybeBottomSheetManagerContext();
|
|
48
46
|
|
|
49
47
|
const storeOpen = useOpen();
|
|
50
|
-
const startClosing = useStartClosing();
|
|
51
48
|
const storeUpdateParams = useUpdateParams();
|
|
52
49
|
|
|
53
50
|
const open = (options?: OpenOptions<T>) => {
|
|
@@ -74,7 +71,12 @@ export function useBottomSheetControl<T extends BottomSheetPortalId>(
|
|
|
74
71
|
};
|
|
75
72
|
|
|
76
73
|
const close = () => {
|
|
77
|
-
|
|
74
|
+
requestClose(id);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const closeAll = (options?: CloseAllOptions) => {
|
|
78
|
+
const groupId = bottomSheetManagerContext?.groupId || 'default';
|
|
79
|
+
return closeAllAnimated(groupId, options);
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
const updateParams = (params: BottomSheetPortalParams<T>) => {
|
|
@@ -88,6 +90,7 @@ export function useBottomSheetControl<T extends BottomSheetPortalId>(
|
|
|
88
90
|
return {
|
|
89
91
|
open: open as OpenFunction<T>,
|
|
90
92
|
close,
|
|
93
|
+
closeAll,
|
|
91
94
|
updateParams,
|
|
92
95
|
resetParams,
|
|
93
96
|
};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
useOpen,
|
|
5
|
-
useStartClosing,
|
|
6
|
-
useClearGroup,
|
|
7
|
-
type OpenMode,
|
|
8
|
-
} from './bottomSheet.store';
|
|
3
|
+
import { useOpen, useClearGroup, type OpenMode } from './bottomSheet.store';
|
|
9
4
|
import { useMaybeBottomSheetManagerContext } from './BottomSheetManager.provider';
|
|
10
5
|
import type { SheetAdapterRef } from './adapter.types';
|
|
6
|
+
import { closeAllAnimated, requestClose } from './bottomSheetCoordinator';
|
|
11
7
|
import { setSheetRef } from './refsMap';
|
|
12
8
|
|
|
9
|
+
export interface CloseAllOptions {
|
|
10
|
+
/** Delay in ms between each cascading close animation. Default: 100 */
|
|
11
|
+
stagger?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
13
14
|
export const useBottomSheetManager = () => {
|
|
14
15
|
const bottomSheetManagerContext = useMaybeBottomSheetManagerContext();
|
|
15
16
|
|
|
16
17
|
const storeOpen = useOpen();
|
|
17
|
-
const startClosing = useStartClosing();
|
|
18
18
|
const storeClearGroup = useClearGroup();
|
|
19
19
|
|
|
20
20
|
const openBottomSheet = (
|
|
@@ -52,7 +52,12 @@ export const useBottomSheetManager = () => {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const close = (id: string) => {
|
|
55
|
-
|
|
55
|
+
requestClose(id);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const closeAll = (options?: CloseAllOptions) => {
|
|
59
|
+
const groupId = bottomSheetManagerContext?.groupId || 'default';
|
|
60
|
+
return closeAllAnimated(groupId, options);
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
const clear = () => {
|
|
@@ -63,6 +68,7 @@ export const useBottomSheetManager = () => {
|
|
|
63
68
|
return {
|
|
64
69
|
open: openBottomSheet,
|
|
65
70
|
close,
|
|
71
|
+
closeAll,
|
|
66
72
|
clear,
|
|
67
73
|
/** @deprecated Use `open` instead */
|
|
68
74
|
openBottomSheet,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useMaybeBottomSheetContext } from './BottomSheet.context';
|
|
4
|
+
import { useSetPreventDismiss } from './bottomSheet.store';
|
|
5
|
+
import type { OnBeforeCloseCallback } from './onBeforeCloseRegistry';
|
|
6
|
+
import { removeOnBeforeClose, setOnBeforeClose } from './onBeforeCloseRegistry';
|
|
7
|
+
import { useEvent } from './useEvent';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers an interceptor that is called before the sheet closes.
|
|
11
|
+
*
|
|
12
|
+
* When active, this hook:
|
|
13
|
+
* 1. Sets `preventDismiss` on the sheet so adapters block user-initiated
|
|
14
|
+
* gestures (swipe down, pan-to-close) at the native level.
|
|
15
|
+
* 2. Intercepts all programmatic close paths (backdrop tap, back button,
|
|
16
|
+
* `close()`, `closeAll()`) and calls the callback first.
|
|
17
|
+
*
|
|
18
|
+
* The interceptor receives `onConfirm` and `onCancel` callbacks. Call these
|
|
19
|
+
* when the user makes a decision. This works seamlessly with `Alert.alert`:
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* useOnBeforeClose(({ onConfirm, onCancel }) => {
|
|
23
|
+
* if (dirty) {
|
|
24
|
+
* Alert.alert('Discard changes?', '', [
|
|
25
|
+
* { text: 'Cancel', onPress: onCancel },
|
|
26
|
+
* { text: 'Discard', onPress: onConfirm },
|
|
27
|
+
* ]);
|
|
28
|
+
* } else {
|
|
29
|
+
* onConfirm(); // Allow close immediately
|
|
30
|
+
* }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* For backward compatibility, you can still return `boolean` or `Promise<boolean>`:
|
|
35
|
+
* - Return `false` (or resolve to `false`) to prevent closing
|
|
36
|
+
* - Return `true` (or resolve to `true`) to allow closing
|
|
37
|
+
*
|
|
38
|
+
* Use `forceClose()` from `useBottomSheetContext` to bypass the interceptor entirely.
|
|
39
|
+
*
|
|
40
|
+
* Must be used inside a sheet component (within BottomSheetContext).
|
|
41
|
+
*
|
|
42
|
+
* @example Callback pattern (recommended)
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function MySheet() {
|
|
45
|
+
* const [dirty, setDirty] = useState(false);
|
|
46
|
+
*
|
|
47
|
+
* useOnBeforeClose(({ onConfirm, onCancel }) => {
|
|
48
|
+
* if (dirty) {
|
|
49
|
+
* Alert.alert('Discard changes?', '', [
|
|
50
|
+
* { text: 'Cancel', style: 'cancel', onPress: onCancel },
|
|
51
|
+
* { text: 'Discard', onPress: onConfirm },
|
|
52
|
+
* ]);
|
|
53
|
+
* } else {
|
|
54
|
+
* onConfirm();
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example Boolean return (backward compatible)
|
|
61
|
+
* ```tsx
|
|
62
|
+
* function MySheet() {
|
|
63
|
+
* const [dirty, setDirty] = useState(false);
|
|
64
|
+
*
|
|
65
|
+
* useOnBeforeClose(() => {
|
|
66
|
+
* return !dirty; // false blocks, true allows
|
|
67
|
+
* });
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function useOnBeforeClose(callback: OnBeforeCloseCallback): void {
|
|
72
|
+
const context = useMaybeBottomSheetContext();
|
|
73
|
+
const setPreventDismiss = useSetPreventDismiss();
|
|
74
|
+
|
|
75
|
+
if (!context?.id) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'useOnBeforeClose must be used within a BottomSheet component'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const id = context.id;
|
|
82
|
+
const stableCallback = useEvent(callback);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setOnBeforeClose(id, stableCallback);
|
|
86
|
+
setPreventDismiss(id, true);
|
|
87
|
+
return () => {
|
|
88
|
+
removeOnBeforeClose(id);
|
|
89
|
+
setPreventDismiss(id, false);
|
|
90
|
+
};
|
|
91
|
+
}, [id, stableCallback, setPreventDismiss]);
|
|
92
|
+
}
|