zkit-ui 2.0.16 → 2.0.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"nativeBottom.d.ts","sourceRoot":"","sources":["../../../src/ui/Sheet/nativeBottom.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"nativeBottom.d.ts","sourceRoot":"","sources":["../../../src/ui/Sheet/nativeBottom.tsx"],"names":[],"mappings":"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"}
@@ -170,7 +170,7 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
170
170
  const [sheetState, setSheetState] = React.useState(isOpen ? 'opening' : 'closed');
171
171
  const [shellMounted, setShellMounted] = React.useState(isOpen);
172
172
  const nativeRef = React.useRef(null);
173
- const phaseRef = React.useRef(isOpen ? 'presenting' : 'idle');
173
+ const phaseRef = React.useRef('idle');
174
174
  const pendingDismissRef = React.useRef(false);
175
175
  const activeLifecycleRef = React.useRef(isOpen);
176
176
  const isOpenRef = React.useRef(isOpen);
@@ -180,8 +180,16 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
180
180
  const presentAnimatedRef = React.useRef(true);
181
181
  const dismissAnimatedRef = React.useRef(true);
182
182
  const closeReasonRef = React.useRef('system');
183
+ const hasRequestedCloseReasonRef = React.useRef(false);
184
+ const nativeOperationIdRef = React.useRef(0);
183
185
  const backdropConfig = React.useMemo(() => resolveBackdrop(backdrop), [backdrop]);
184
186
  const usesManualBackdrop = react_native_1.Platform.OS === 'ios' && backdropConfig.visible;
187
+ // The outer RN Modal owns a separate native presentation lifecycle on iOS.
188
+ // Keep it mounted through onDismiss so a rapid reopen cannot overlap that dismissal.
189
+ const [manualModalVisible, setManualModalVisible] = React.useState(usesManualBackdrop && isOpen);
190
+ const [manualModalReady, setManualModalReady] = React.useState(false);
191
+ const manualModalPhaseRef = React.useRef(usesManualBackdrop && isOpen ? 'showing' : 'hidden');
192
+ const manualCloseShouldSyncRef = React.useRef(false);
185
193
  const backdropOpacity = React.useRef(new react_native_1.Animated.Value(usesManualBackdrop && isOpen ? backdropConfig.opacity : 0)).current;
186
194
  const [manualBackdropMounted, setManualBackdropMounted] = React.useState(usesManualBackdrop && isOpen);
187
195
  React.useEffect(() => {
@@ -193,38 +201,6 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
193
201
  React.useEffect(() => {
194
202
  currentDetentIndexRef.current = currentDetentIndex;
195
203
  }, [currentDetentIndex]);
196
- React.useEffect(() => {
197
- if (isOpen) {
198
- activeLifecycleRef.current = true;
199
- requestedOpenIndexRef.current = controlledDetentIndex !== null && controlledDetentIndex !== void 0 ? controlledDetentIndex : requestedOpenIndexRef.current;
200
- if (!shellMounted)
201
- setShellMounted(true);
202
- return;
203
- }
204
- if (shellMounted) {
205
- requestDismiss('system');
206
- }
207
- }, [controlledDetentIndex, isOpen, shellMounted]);
208
- React.useEffect(() => {
209
- if (!isOpen || !shellMounted)
210
- return undefined;
211
- const rafId = requestAnimationFrame(() => {
212
- if (!isOpenRef.current)
213
- return;
214
- if (phaseRef.current !== 'idle' && phaseRef.current !== 'presenting')
215
- return;
216
- const sheet = nativeRef.current;
217
- if (!sheet)
218
- return;
219
- const targetIndex = normalizeDetentIndex(controlledDetentIndex !== null && controlledDetentIndex !== void 0 ? controlledDetentIndex : requestedOpenIndexRef.current, nativeDetents.length);
220
- pendingDismissRef.current = false;
221
- phaseRef.current = 'presenting';
222
- setSheetState('opening');
223
- setCurrentDetentIndex(targetIndex);
224
- silentlyCatchPromise(sheet.present(targetIndex, presentAnimatedRef.current));
225
- });
226
- return () => cancelAnimationFrame(rafId);
227
- }, [controlledDetentIndex, isOpen, nativeDetents.length, shellMounted]);
228
204
  React.useEffect(() => {
229
205
  var _a;
230
206
  if (controlledDetentIndex == null)
@@ -275,28 +251,34 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
275
251
  presentAnimatedRef.current = true;
276
252
  activeLifecycleRef.current = true;
277
253
  pendingDismissRef.current = false;
254
+ hasRequestedCloseReasonRef.current = false;
278
255
  if (!isOpenControlled)
279
256
  setInnerOpen(true);
280
257
  emitOpenChange(true, 'api', requestedOpenIndexRef.current);
281
258
  return;
282
259
  }
283
260
  closeReasonRef.current = reason === 'api' ? 'api' : reason;
261
+ hasRequestedCloseReasonRef.current = true;
284
262
  if (!isOpenControlled)
285
263
  setInnerOpen(false);
286
264
  emitOpenChange(false, reason, currentDetentIndexRef.current);
287
265
  }, [disabled, emitOpenChange, isOpenControlled, nativeDetents.length]);
288
- const finishClosedLifecycle = React.useCallback((shouldSyncOpenState) => {
266
+ const finishClosedLifecycle = React.useCallback((shouldSyncOpenState, keepShellMounted = false) => {
267
+ nativeOperationIdRef.current += 1;
289
268
  phaseRef.current = 'idle';
290
269
  pendingDismissRef.current = false;
291
270
  presentAnimatedRef.current = true;
292
271
  dismissAnimatedRef.current = true;
272
+ setManualModalReady(false);
293
273
  setSheetState('closed');
294
274
  if (shouldSyncOpenState) {
295
275
  if (!isOpenControlled)
296
276
  setInnerOpen(false);
297
277
  emitOpenChange(false, closeReasonRef.current, currentDetentIndexRef.current);
298
278
  }
299
- setShellMounted(false);
279
+ if (!keepShellMounted) {
280
+ setShellMounted(false);
281
+ }
300
282
  if (activeLifecycleRef.current) {
301
283
  activeLifecycleRef.current = false;
302
284
  onCloseComplete === null || onCloseComplete === void 0 ? void 0 : onCloseComplete({
@@ -305,31 +287,158 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
305
287
  });
306
288
  }
307
289
  }, [emitOpenChange, isOpenControlled, onCloseComplete]);
308
- function requestDismiss(reason) {
290
+ const startManualModalDismiss = React.useCallback((shouldSyncOpenState) => {
291
+ manualCloseShouldSyncRef.current =
292
+ manualCloseShouldSyncRef.current || shouldSyncOpenState;
293
+ nativeOperationIdRef.current += 1;
294
+ phaseRef.current = 'idle';
295
+ pendingDismissRef.current = false;
296
+ setManualModalReady(false);
297
+ setSheetState('closing');
298
+ if (manualModalPhaseRef.current === 'hidden') {
299
+ finishClosedLifecycle(manualCloseShouldSyncRef.current);
300
+ manualCloseShouldSyncRef.current = false;
301
+ return;
302
+ }
303
+ if (manualModalPhaseRef.current !== 'hiding') {
304
+ manualModalPhaseRef.current = 'hiding';
305
+ setManualModalVisible(false);
306
+ }
307
+ }, [finishClosedLifecycle]);
308
+ const recoverNativeOperation = React.useCallback((operation) => {
309
+ if (operation === 'present') {
310
+ closeReasonRef.current = 'system';
311
+ }
312
+ const shouldSyncOpenState = isOpenRef.current;
313
+ if (usesManualBackdrop) {
314
+ startManualModalDismiss(shouldSyncOpenState);
315
+ return;
316
+ }
317
+ finishClosedLifecycle(shouldSyncOpenState);
318
+ }, [finishClosedLifecycle, startManualModalDismiss, usesManualBackdrop]);
319
+ const requestDismiss = React.useCallback((reason) => {
309
320
  const phase = phaseRef.current;
310
- closeReasonRef.current = reason;
311
- if (phase === 'dismissing')
321
+ if (phase === 'dismissing' ||
322
+ (usesManualBackdrop && manualModalPhaseRef.current === 'hiding')) {
312
323
  return;
324
+ }
313
325
  if (phase === 'presenting') {
326
+ if (!pendingDismissRef.current) {
327
+ closeReasonRef.current = reason;
328
+ }
314
329
  pendingDismissRef.current = true;
315
330
  return;
316
331
  }
332
+ closeReasonRef.current = reason;
317
333
  if (phase === 'presented') {
318
334
  const sheet = nativeRef.current;
319
335
  if (!sheet) {
320
- finishClosedLifecycle(false);
336
+ if (usesManualBackdrop) {
337
+ startManualModalDismiss(false);
338
+ }
339
+ else {
340
+ finishClosedLifecycle(false);
341
+ }
321
342
  return;
322
343
  }
323
344
  pendingDismissRef.current = false;
324
345
  phaseRef.current = 'dismissing';
325
346
  setSheetState('closing');
326
- silentlyCatchPromise(sheet.dismiss(dismissAnimatedRef.current));
347
+ const operationId = ++nativeOperationIdRef.current;
348
+ void sheet.dismiss(dismissAnimatedRef.current).catch(() => {
349
+ if (operationId !== nativeOperationIdRef.current ||
350
+ phaseRef.current !== 'dismissing') {
351
+ return;
352
+ }
353
+ recoverNativeOperation('dismiss');
354
+ });
327
355
  return;
328
356
  }
329
- if (activeLifecycleRef.current) {
357
+ if (!activeLifecycleRef.current)
358
+ return;
359
+ if (usesManualBackdrop) {
360
+ startManualModalDismiss(false);
361
+ }
362
+ else {
330
363
  finishClosedLifecycle(false);
331
364
  }
332
- }
365
+ }, [
366
+ finishClosedLifecycle,
367
+ recoverNativeOperation,
368
+ startManualModalDismiss,
369
+ usesManualBackdrop,
370
+ ]);
371
+ React.useEffect(() => {
372
+ if (isOpen) {
373
+ activeLifecycleRef.current = true;
374
+ requestedOpenIndexRef.current = controlledDetentIndex !== null && controlledDetentIndex !== void 0 ? controlledDetentIndex : requestedOpenIndexRef.current;
375
+ if (!shellMounted) {
376
+ setShellMounted(true);
377
+ }
378
+ if (usesManualBackdrop && manualModalPhaseRef.current === 'hidden') {
379
+ manualCloseShouldSyncRef.current = false;
380
+ manualModalPhaseRef.current = 'showing';
381
+ setManualModalReady(false);
382
+ setManualModalVisible(true);
383
+ }
384
+ return;
385
+ }
386
+ if (shellMounted) {
387
+ const reason = hasRequestedCloseReasonRef.current ? closeReasonRef.current : 'system';
388
+ hasRequestedCloseReasonRef.current = false;
389
+ requestDismiss(reason);
390
+ }
391
+ }, [
392
+ controlledDetentIndex,
393
+ isOpen,
394
+ requestDismiss,
395
+ shellMounted,
396
+ usesManualBackdrop,
397
+ ]);
398
+ React.useEffect(() => {
399
+ if (!isOpen || !shellMounted)
400
+ return undefined;
401
+ if (usesManualBackdrop && !manualModalReady)
402
+ return undefined;
403
+ const present = () => {
404
+ if (!isOpenRef.current || phaseRef.current !== 'idle')
405
+ return;
406
+ if (usesManualBackdrop &&
407
+ manualModalPhaseRef.current !== 'shown') {
408
+ return;
409
+ }
410
+ const sheet = nativeRef.current;
411
+ if (!sheet)
412
+ return;
413
+ const targetIndex = normalizeDetentIndex(controlledDetentIndex !== null && controlledDetentIndex !== void 0 ? controlledDetentIndex : requestedOpenIndexRef.current, nativeDetents.length);
414
+ pendingDismissRef.current = false;
415
+ phaseRef.current = 'presenting';
416
+ setSheetState('opening');
417
+ setCurrentDetentIndex(targetIndex);
418
+ const operationId = ++nativeOperationIdRef.current;
419
+ void sheet.present(targetIndex, presentAnimatedRef.current).catch(() => {
420
+ if (operationId !== nativeOperationIdRef.current ||
421
+ phaseRef.current !== 'presenting') {
422
+ return;
423
+ }
424
+ recoverNativeOperation('present');
425
+ });
426
+ };
427
+ if (usesManualBackdrop) {
428
+ present();
429
+ return undefined;
430
+ }
431
+ const rafId = requestAnimationFrame(present);
432
+ return () => cancelAnimationFrame(rafId);
433
+ }, [
434
+ controlledDetentIndex,
435
+ isOpen,
436
+ manualModalReady,
437
+ nativeDetents.length,
438
+ recoverNativeOperation,
439
+ shellMounted,
440
+ usesManualBackdrop,
441
+ ]);
333
442
  const openSheet = React.useCallback(async (options) => {
334
443
  var _a, _b;
335
444
  const targetIndex = normalizeDetentIndex((_a = options === null || options === void 0 ? void 0 : options.detentIndex) !== null && _a !== void 0 ? _a : requestedOpenIndexRef.current, nativeDetents.length);
@@ -378,14 +487,50 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
378
487
  if (pendingDismissRef.current || !isOpenRef.current) {
379
488
  requestDismiss(closeReasonRef.current);
380
489
  }
381
- }, [nativeDetents, onOpenComplete, semanticDetents]);
490
+ }, [nativeDetents, onOpenComplete, requestDismiss, semanticDetents]);
382
491
  const handleDidDismiss = React.useCallback(() => {
383
492
  const wasRequestedDismiss = phaseRef.current === 'dismissing' || pendingDismissRef.current;
384
493
  const shouldSyncOpenState = isOpenRef.current && !wasRequestedDismiss;
385
494
  if (shouldSyncOpenState) {
386
495
  closeReasonRef.current = closeReasonRef.current === 'system' ? 'gesture' : closeReasonRef.current;
387
496
  }
388
- finishClosedLifecycle(shouldSyncOpenState);
497
+ if (usesManualBackdrop) {
498
+ startManualModalDismiss(shouldSyncOpenState);
499
+ }
500
+ else {
501
+ finishClosedLifecycle(shouldSyncOpenState);
502
+ }
503
+ }, [finishClosedLifecycle, startManualModalDismiss, usesManualBackdrop]);
504
+ const handleManualModalShow = React.useCallback(() => {
505
+ if (manualModalPhaseRef.current !== 'showing')
506
+ return;
507
+ if (!isOpenRef.current) {
508
+ startManualModalDismiss(false);
509
+ return;
510
+ }
511
+ manualModalPhaseRef.current = 'shown';
512
+ // onShow is the native readiness barrier for presenting the nested TrueSheet.
513
+ setManualModalReady(true);
514
+ }, [startManualModalDismiss]);
515
+ const handleManualModalDismiss = React.useCallback(() => {
516
+ const shouldSyncOpenState = manualCloseShouldSyncRef.current;
517
+ const shouldReopen = isOpenRef.current && !shouldSyncOpenState;
518
+ const reopenAnimated = presentAnimatedRef.current;
519
+ manualModalPhaseRef.current = 'hidden';
520
+ manualCloseShouldSyncRef.current = false;
521
+ setManualModalReady(false);
522
+ setManualModalVisible(false);
523
+ // Only onDismiss guarantees that UIKit has removed the outer presenter.
524
+ finishClosedLifecycle(shouldSyncOpenState, shouldReopen);
525
+ if (!shouldReopen)
526
+ return;
527
+ activeLifecycleRef.current = true;
528
+ hasRequestedCloseReasonRef.current = false;
529
+ presentAnimatedRef.current = reopenAnimated;
530
+ phaseRef.current = 'idle';
531
+ manualModalPhaseRef.current = 'showing';
532
+ setSheetState('opening');
533
+ setManualModalVisible(true);
389
534
  }, [finishClosedLifecycle]);
390
535
  const handleDetentChange = React.useCallback((event) => {
391
536
  const payload = createDetentPayload(event.nativeEvent, semanticDetents, nativeDetents);
@@ -425,7 +570,7 @@ exports.NativeBottomSheet = React.forwardRef(function NativeBottomSheet({ open:
425
570
  if (!shellMounted)
426
571
  return null;
427
572
  if (usesManualBackdrop) {
428
- return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: true, transparent: true, animationType: "none", statusBarTranslucent: true, presentationStyle: "overFullScreen", onRequestClose: handleBackdropPress, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.modalRoot, pointerEvents: "box-none", children: [manualBackdropMounted ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: backdropAccessibilityLabel, disabled: disabled || !dismissible || !backdropConfig.dismissOnPress, onPress: handleBackdropPress, style: react_native_1.StyleSheet.absoluteFill, children: (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { pointerEvents: "none", style: [
573
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: manualModalVisible, transparent: true, animationType: "none", statusBarTranslucent: true, presentationStyle: "overFullScreen", onShow: handleManualModalShow, onDismiss: handleManualModalDismiss, onRequestClose: handleBackdropPress, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.modalRoot, pointerEvents: "box-none", children: [manualBackdropMounted ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: backdropAccessibilityLabel, disabled: disabled || !dismissible || !backdropConfig.dismissOnPress, onPress: handleBackdropPress, style: react_native_1.StyleSheet.absoluteFill, children: (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { pointerEvents: "none", style: [
429
574
  react_native_1.StyleSheet.absoluteFill,
430
575
  {
431
576
  backgroundColor: backdropConfig.color,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zkit-ui",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -92,6 +92,7 @@ export type NativeBottomSheetProps = {
92
92
  };
93
93
 
94
94
  type NativePhase = 'idle' | 'presenting' | 'presented' | 'dismissing';
95
+ type ManualModalPhase = 'hidden' | 'showing' | 'shown' | 'hiding';
95
96
  type ResolvedBackdrop = Required<SheetBackdropConfig>;
96
97
 
97
98
  const DEFAULT_DETENTS: readonly SheetDetent[] = ['content'];
@@ -272,7 +273,7 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
272
273
  const [shellMounted, setShellMounted] = React.useState(isOpen);
273
274
 
274
275
  const nativeRef = React.useRef<TrueSheet>(null);
275
- const phaseRef = React.useRef<NativePhase>(isOpen ? 'presenting' : 'idle');
276
+ const phaseRef = React.useRef<NativePhase>('idle');
276
277
  const pendingDismissRef = React.useRef(false);
277
278
  const activeLifecycleRef = React.useRef(isOpen);
278
279
  const isOpenRef = React.useRef(isOpen);
@@ -282,8 +283,18 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
282
283
  const presentAnimatedRef = React.useRef(true);
283
284
  const dismissAnimatedRef = React.useRef(true);
284
285
  const closeReasonRef = React.useRef<SheetCloseReason>('system');
286
+ const hasRequestedCloseReasonRef = React.useRef(false);
287
+ const nativeOperationIdRef = React.useRef(0);
285
288
  const backdropConfig = React.useMemo(() => resolveBackdrop(backdrop), [backdrop]);
286
289
  const usesManualBackdrop = Platform.OS === 'ios' && backdropConfig.visible;
290
+ // The outer RN Modal owns a separate native presentation lifecycle on iOS.
291
+ // Keep it mounted through onDismiss so a rapid reopen cannot overlap that dismissal.
292
+ const [manualModalVisible, setManualModalVisible] = React.useState(usesManualBackdrop && isOpen);
293
+ const [manualModalReady, setManualModalReady] = React.useState(false);
294
+ const manualModalPhaseRef = React.useRef<ManualModalPhase>(
295
+ usesManualBackdrop && isOpen ? 'showing' : 'hidden'
296
+ );
297
+ const manualCloseShouldSyncRef = React.useRef(false);
287
298
  const backdropOpacity = React.useRef(
288
299
  new Animated.Value(usesManualBackdrop && isOpen ? backdropConfig.opacity : 0)
289
300
  ).current;
@@ -301,44 +312,6 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
301
312
  currentDetentIndexRef.current = currentDetentIndex;
302
313
  }, [currentDetentIndex]);
303
314
 
304
- React.useEffect(() => {
305
- if (isOpen) {
306
- activeLifecycleRef.current = true;
307
- requestedOpenIndexRef.current = controlledDetentIndex ?? requestedOpenIndexRef.current;
308
- if (!shellMounted) setShellMounted(true);
309
- return;
310
- }
311
-
312
- if (shellMounted) {
313
- requestDismiss('system');
314
- }
315
- }, [controlledDetentIndex, isOpen, shellMounted]);
316
-
317
- React.useEffect(() => {
318
- if (!isOpen || !shellMounted) return undefined;
319
-
320
- const rafId = requestAnimationFrame(() => {
321
- if (!isOpenRef.current) return;
322
- if (phaseRef.current !== 'idle' && phaseRef.current !== 'presenting') return;
323
-
324
- const sheet = nativeRef.current;
325
- if (!sheet) return;
326
-
327
- const targetIndex = normalizeDetentIndex(
328
- controlledDetentIndex ?? requestedOpenIndexRef.current,
329
- nativeDetents.length
330
- );
331
-
332
- pendingDismissRef.current = false;
333
- phaseRef.current = 'presenting';
334
- setSheetState('opening');
335
- setCurrentDetentIndex(targetIndex);
336
- silentlyCatchPromise(sheet.present(targetIndex, presentAnimatedRef.current));
337
- });
338
-
339
- return () => cancelAnimationFrame(rafId);
340
- }, [controlledDetentIndex, isOpen, nativeDetents.length, shellMounted]);
341
-
342
315
  React.useEffect(() => {
343
316
  if (controlledDetentIndex == null) return;
344
317
  requestedOpenIndexRef.current = controlledDetentIndex;
@@ -399,12 +372,14 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
399
372
  presentAnimatedRef.current = true;
400
373
  activeLifecycleRef.current = true;
401
374
  pendingDismissRef.current = false;
375
+ hasRequestedCloseReasonRef.current = false;
402
376
  if (!isOpenControlled) setInnerOpen(true);
403
377
  emitOpenChange(true, 'api', requestedOpenIndexRef.current);
404
378
  return;
405
379
  }
406
380
 
407
381
  closeReasonRef.current = reason === 'api' ? 'api' : reason;
382
+ hasRequestedCloseReasonRef.current = true;
408
383
  if (!isOpenControlled) setInnerOpen(false);
409
384
  emitOpenChange(false, reason, currentDetentIndexRef.current);
410
385
  },
@@ -412,11 +387,13 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
412
387
  );
413
388
 
414
389
  const finishClosedLifecycle = React.useCallback(
415
- (shouldSyncOpenState: boolean) => {
390
+ (shouldSyncOpenState: boolean, keepShellMounted = false) => {
391
+ nativeOperationIdRef.current += 1;
416
392
  phaseRef.current = 'idle';
417
393
  pendingDismissRef.current = false;
418
394
  presentAnimatedRef.current = true;
419
395
  dismissAnimatedRef.current = true;
396
+ setManualModalReady(false);
420
397
  setSheetState('closed');
421
398
 
422
399
  if (shouldSyncOpenState) {
@@ -424,7 +401,9 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
424
401
  emitOpenChange(false, closeReasonRef.current, currentDetentIndexRef.current);
425
402
  }
426
403
 
427
- setShellMounted(false);
404
+ if (!keepShellMounted) {
405
+ setShellMounted(false);
406
+ }
428
407
 
429
408
  if (activeLifecycleRef.current) {
430
409
  activeLifecycleRef.current = false;
@@ -437,35 +416,195 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
437
416
  [emitOpenChange, isOpenControlled, onCloseComplete]
438
417
  );
439
418
 
440
- function requestDismiss(reason: SheetCloseReason) {
441
- const phase = phaseRef.current;
442
- closeReasonRef.current = reason;
419
+ const startManualModalDismiss = React.useCallback(
420
+ (shouldSyncOpenState: boolean) => {
421
+ manualCloseShouldSyncRef.current =
422
+ manualCloseShouldSyncRef.current || shouldSyncOpenState;
423
+ nativeOperationIdRef.current += 1;
424
+ phaseRef.current = 'idle';
425
+ pendingDismissRef.current = false;
426
+ setManualModalReady(false);
427
+ setSheetState('closing');
428
+
429
+ if (manualModalPhaseRef.current === 'hidden') {
430
+ finishClosedLifecycle(manualCloseShouldSyncRef.current);
431
+ manualCloseShouldSyncRef.current = false;
432
+ return;
433
+ }
434
+
435
+ if (manualModalPhaseRef.current !== 'hiding') {
436
+ manualModalPhaseRef.current = 'hiding';
437
+ setManualModalVisible(false);
438
+ }
439
+ },
440
+ [finishClosedLifecycle]
441
+ );
442
+
443
+ const recoverNativeOperation = React.useCallback(
444
+ (operation: 'present' | 'dismiss') => {
445
+ if (operation === 'present') {
446
+ closeReasonRef.current = 'system';
447
+ }
448
+ const shouldSyncOpenState = isOpenRef.current;
449
+
450
+ if (usesManualBackdrop) {
451
+ startManualModalDismiss(shouldSyncOpenState);
452
+ return;
453
+ }
454
+
455
+ finishClosedLifecycle(shouldSyncOpenState);
456
+ },
457
+ [finishClosedLifecycle, startManualModalDismiss, usesManualBackdrop]
458
+ );
459
+
460
+ const requestDismiss = React.useCallback(
461
+ (reason: SheetCloseReason) => {
462
+ const phase = phaseRef.current;
443
463
 
444
- if (phase === 'dismissing') return;
464
+ if (
465
+ phase === 'dismissing' ||
466
+ (usesManualBackdrop && manualModalPhaseRef.current === 'hiding')
467
+ ) {
468
+ return;
469
+ }
445
470
 
446
- if (phase === 'presenting') {
447
- pendingDismissRef.current = true;
471
+ if (phase === 'presenting') {
472
+ if (!pendingDismissRef.current) {
473
+ closeReasonRef.current = reason;
474
+ }
475
+ pendingDismissRef.current = true;
476
+ return;
477
+ }
478
+
479
+ closeReasonRef.current = reason;
480
+
481
+ if (phase === 'presented') {
482
+ const sheet = nativeRef.current;
483
+ if (!sheet) {
484
+ if (usesManualBackdrop) {
485
+ startManualModalDismiss(false);
486
+ } else {
487
+ finishClosedLifecycle(false);
488
+ }
489
+ return;
490
+ }
491
+
492
+ pendingDismissRef.current = false;
493
+ phaseRef.current = 'dismissing';
494
+ setSheetState('closing');
495
+ const operationId = ++nativeOperationIdRef.current;
496
+ void sheet.dismiss(dismissAnimatedRef.current).catch(() => {
497
+ if (
498
+ operationId !== nativeOperationIdRef.current ||
499
+ phaseRef.current !== 'dismissing'
500
+ ) {
501
+ return;
502
+ }
503
+ recoverNativeOperation('dismiss');
504
+ });
505
+ return;
506
+ }
507
+
508
+ if (!activeLifecycleRef.current) return;
509
+
510
+ if (usesManualBackdrop) {
511
+ startManualModalDismiss(false);
512
+ } else {
513
+ finishClosedLifecycle(false);
514
+ }
515
+ },
516
+ [
517
+ finishClosedLifecycle,
518
+ recoverNativeOperation,
519
+ startManualModalDismiss,
520
+ usesManualBackdrop,
521
+ ]
522
+ );
523
+
524
+ React.useEffect(() => {
525
+ if (isOpen) {
526
+ activeLifecycleRef.current = true;
527
+ requestedOpenIndexRef.current = controlledDetentIndex ?? requestedOpenIndexRef.current;
528
+
529
+ if (!shellMounted) {
530
+ setShellMounted(true);
531
+ }
532
+
533
+ if (usesManualBackdrop && manualModalPhaseRef.current === 'hidden') {
534
+ manualCloseShouldSyncRef.current = false;
535
+ manualModalPhaseRef.current = 'showing';
536
+ setManualModalReady(false);
537
+ setManualModalVisible(true);
538
+ }
448
539
  return;
449
540
  }
450
541
 
451
- if (phase === 'presented') {
452
- const sheet = nativeRef.current;
453
- if (!sheet) {
454
- finishClosedLifecycle(false);
542
+ if (shellMounted) {
543
+ const reason = hasRequestedCloseReasonRef.current ? closeReasonRef.current : 'system';
544
+ hasRequestedCloseReasonRef.current = false;
545
+ requestDismiss(reason);
546
+ }
547
+ }, [
548
+ controlledDetentIndex,
549
+ isOpen,
550
+ requestDismiss,
551
+ shellMounted,
552
+ usesManualBackdrop,
553
+ ]);
554
+
555
+ React.useEffect(() => {
556
+ if (!isOpen || !shellMounted) return undefined;
557
+ if (usesManualBackdrop && !manualModalReady) return undefined;
558
+
559
+ const present = () => {
560
+ if (!isOpenRef.current || phaseRef.current !== 'idle') return;
561
+ if (
562
+ usesManualBackdrop &&
563
+ manualModalPhaseRef.current !== 'shown'
564
+ ) {
455
565
  return;
456
566
  }
457
567
 
568
+ const sheet = nativeRef.current;
569
+ if (!sheet) return;
570
+
571
+ const targetIndex = normalizeDetentIndex(
572
+ controlledDetentIndex ?? requestedOpenIndexRef.current,
573
+ nativeDetents.length
574
+ );
575
+
458
576
  pendingDismissRef.current = false;
459
- phaseRef.current = 'dismissing';
460
- setSheetState('closing');
461
- silentlyCatchPromise(sheet.dismiss(dismissAnimatedRef.current));
462
- return;
463
- }
577
+ phaseRef.current = 'presenting';
578
+ setSheetState('opening');
579
+ setCurrentDetentIndex(targetIndex);
580
+ const operationId = ++nativeOperationIdRef.current;
581
+ void sheet.present(targetIndex, presentAnimatedRef.current).catch(() => {
582
+ if (
583
+ operationId !== nativeOperationIdRef.current ||
584
+ phaseRef.current !== 'presenting'
585
+ ) {
586
+ return;
587
+ }
588
+ recoverNativeOperation('present');
589
+ });
590
+ };
464
591
 
465
- if (activeLifecycleRef.current) {
466
- finishClosedLifecycle(false);
592
+ if (usesManualBackdrop) {
593
+ present();
594
+ return undefined;
467
595
  }
468
- }
596
+
597
+ const rafId = requestAnimationFrame(present);
598
+ return () => cancelAnimationFrame(rafId);
599
+ }, [
600
+ controlledDetentIndex,
601
+ isOpen,
602
+ manualModalReady,
603
+ nativeDetents.length,
604
+ recoverNativeOperation,
605
+ shellMounted,
606
+ usesManualBackdrop,
607
+ ]);
469
608
 
470
609
  const openSheet = React.useCallback(
471
610
  async (options?: SheetOpenOptions) => {
@@ -541,7 +680,7 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
541
680
  requestDismiss(closeReasonRef.current);
542
681
  }
543
682
  },
544
- [nativeDetents, onOpenComplete, semanticDetents]
683
+ [nativeDetents, onOpenComplete, requestDismiss, semanticDetents]
545
684
  );
546
685
 
547
686
  const handleDidDismiss = React.useCallback(() => {
@@ -550,7 +689,48 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
550
689
  if (shouldSyncOpenState) {
551
690
  closeReasonRef.current = closeReasonRef.current === 'system' ? 'gesture' : closeReasonRef.current;
552
691
  }
553
- finishClosedLifecycle(shouldSyncOpenState);
692
+
693
+ if (usesManualBackdrop) {
694
+ startManualModalDismiss(shouldSyncOpenState);
695
+ } else {
696
+ finishClosedLifecycle(shouldSyncOpenState);
697
+ }
698
+ }, [finishClosedLifecycle, startManualModalDismiss, usesManualBackdrop]);
699
+
700
+ const handleManualModalShow = React.useCallback(() => {
701
+ if (manualModalPhaseRef.current !== 'showing') return;
702
+
703
+ if (!isOpenRef.current) {
704
+ startManualModalDismiss(false);
705
+ return;
706
+ }
707
+
708
+ manualModalPhaseRef.current = 'shown';
709
+ // onShow is the native readiness barrier for presenting the nested TrueSheet.
710
+ setManualModalReady(true);
711
+ }, [startManualModalDismiss]);
712
+
713
+ const handleManualModalDismiss = React.useCallback(() => {
714
+ const shouldSyncOpenState = manualCloseShouldSyncRef.current;
715
+ const shouldReopen = isOpenRef.current && !shouldSyncOpenState;
716
+ const reopenAnimated = presentAnimatedRef.current;
717
+
718
+ manualModalPhaseRef.current = 'hidden';
719
+ manualCloseShouldSyncRef.current = false;
720
+ setManualModalReady(false);
721
+ setManualModalVisible(false);
722
+ // Only onDismiss guarantees that UIKit has removed the outer presenter.
723
+ finishClosedLifecycle(shouldSyncOpenState, shouldReopen);
724
+
725
+ if (!shouldReopen) return;
726
+
727
+ activeLifecycleRef.current = true;
728
+ hasRequestedCloseReasonRef.current = false;
729
+ presentAnimatedRef.current = reopenAnimated;
730
+ phaseRef.current = 'idle';
731
+ manualModalPhaseRef.current = 'showing';
732
+ setSheetState('opening');
733
+ setManualModalVisible(true);
554
734
  }, [finishClosedLifecycle]);
555
735
 
556
736
  const handleDetentChange = React.useCallback(
@@ -638,11 +818,13 @@ export const NativeBottomSheet = React.forwardRef<NativeBottomSheetRef, NativeBo
638
818
  if (usesManualBackdrop) {
639
819
  return (
640
820
  <Modal
641
- visible
821
+ visible={manualModalVisible}
642
822
  transparent
643
823
  animationType="none"
644
824
  statusBarTranslucent
645
825
  presentationStyle="overFullScreen"
826
+ onShow={handleManualModalShow}
827
+ onDismiss={handleManualModalDismiss}
646
828
  onRequestClose={handleBackdropPress}
647
829
  >
648
830
  <View style={styles.modalRoot} pointerEvents="box-none">