@hjmds/react-native 1.28.0 → 1.29.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 (48) hide show
  1. package/dist/auth-screen.d.ts +19 -2
  2. package/dist/auth-screen.d.ts.map +1 -1
  3. package/dist/auth-screen.js +41 -24
  4. package/dist/auth-screen.js.map +1 -1
  5. package/dist/collection-rail.d.ts +7 -1
  6. package/dist/collection-rail.d.ts.map +1 -1
  7. package/dist/collection-rail.js +12 -5
  8. package/dist/collection-rail.js.map +1 -1
  9. package/dist/evidence.d.ts +316 -1
  10. package/dist/evidence.d.ts.map +1 -1
  11. package/dist/evidence.js +52 -2
  12. package/dist/evidence.js.map +1 -1
  13. package/dist/feedback.d.ts +7 -1
  14. package/dist/feedback.d.ts.map +1 -1
  15. package/dist/feedback.js +4 -2
  16. package/dist/feedback.js.map +1 -1
  17. package/dist/inputs.d.ts +9 -0
  18. package/dist/inputs.d.ts.map +1 -1
  19. package/dist/inputs.js +4 -1
  20. package/dist/inputs.js.map +1 -1
  21. package/dist/internal/initial-focus.d.ts +44 -0
  22. package/dist/internal/initial-focus.d.ts.map +1 -0
  23. package/dist/internal/initial-focus.js +136 -0
  24. package/dist/internal/initial-focus.js.map +1 -0
  25. package/dist/internal/list-row-scroll-room.d.ts +2 -1
  26. package/dist/internal/list-row-scroll-room.d.ts.map +1 -1
  27. package/dist/internal/list-row-scroll-room.js +2 -1
  28. package/dist/internal/list-row-scroll-room.js.map +1 -1
  29. package/dist/internal/screen-body-inset.d.ts +9 -0
  30. package/dist/internal/screen-body-inset.d.ts.map +1 -0
  31. package/dist/internal/screen-body-inset.js +10 -0
  32. package/dist/internal/screen-body-inset.js.map +1 -0
  33. package/dist/keyboard.d.ts +1 -1
  34. package/dist/keyboard.d.ts.map +1 -1
  35. package/dist/keyboard.js.map +1 -1
  36. package/dist/overlays.d.ts +10 -2
  37. package/dist/overlays.d.ts.map +1 -1
  38. package/dist/overlays.js +255 -210
  39. package/dist/overlays.js.map +1 -1
  40. package/dist/screen-flows.d.ts +9 -4
  41. package/dist/screen-flows.d.ts.map +1 -1
  42. package/dist/screen-flows.js +12 -9
  43. package/dist/screen-flows.js.map +1 -1
  44. package/dist/screens.d.ts +26 -2
  45. package/dist/screens.d.ts.map +1 -1
  46. package/dist/screens.js +22 -6
  47. package/dist/screens.js.map +1 -1
  48. package/package.json +3 -3
package/dist/overlays.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ScreenBodyInsetContext } from "./internal/screen-body-inset.js";
3
+ import { InitialFocusProvider, useInitialFocusScope, useOverlayLayer } from "./internal/initial-focus.js";
4
+ import { initialFocusDefaults } from "@hjmds/design-contracts/screen-patterns";
2
5
  import { resolveOriginTransition } from "@hjmds/design-contracts/content-transition";
3
6
  import { focusAccessibleHost } from "./internal/accessible-focus.js";
4
7
  import { createAlertDialogSession, getAlertDialogInitialFocus, validateAlertDialogRequest, } from "@hjmds/design-contracts/components/alert-dialog";
@@ -26,8 +29,8 @@ function CloseGlyph() {
26
29
  // Keep interaction visibility off native-driven animation hosts. In iOS 26.5,
27
30
  // Dialog/AlertDialog/Sheet lost their AX descendants on a second presentation
28
31
  // while still painting; a non-animated boundary preserves reopen accessibility.
29
- function ModalInteractionBoundary({ visible, children }) {
30
- return _jsx(View, { accessibilityElementsHidden: !visible, importantForAccessibility: visible ? "auto" : "no-hide-descendants", pointerEvents: visible ? "auto" : "none", style: { flex: 1 }, children: children });
32
+ function ModalInteractionBoundary({ visible, onTouchStart, children }) {
33
+ return _jsx(View, { onTouchStart: onTouchStart, accessibilityElementsHidden: !visible, importantForAccessibility: visible ? "auto" : "no-hide-descendants", pointerEvents: visible ? "auto" : "none", style: { flex: 1 }, children: children });
31
34
  }
32
35
  // BT-QA-027: maximum text made a centered BurnTok confirmation taller than the
33
36
  // window, putting its title/cancel off-screen. Keep full copy in a bounded scroll
@@ -149,7 +152,7 @@ function resolveOverlayAccessibleTitle(title, accessibilityTitle) {
149
152
  throw new TypeError("An element `title` requires `accessibilityTitle` for the modal accessible name");
150
153
  }
151
154
  /** Native modal boundary with one reasoned close intent for each user attempt. */
152
- export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTitle, description, children, primaryAction, secondaryAction, dismissible = true, busy: externalBusy = false, onActionError, size = dialogRecipe.defaults.size, motionOrigin, closeLabel, returnFocusRef, contentStyle, onShow, ...modalProps }) {
155
+ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTitle, description, children, primaryAction, secondaryAction, dismissible = true, busy: externalBusy = false, onActionError, size = dialogRecipe.defaults.size, motionOrigin, closeLabel, returnFocusRef, contentStyle, onShow, initialFocus = initialFocusDefaults.overlay, ...modalProps }) {
153
156
  const [actionPending, setActionPending] = useState(null);
154
157
  const actionRun = useRef(null);
155
158
  const busy = externalBusy || actionPending !== null;
@@ -177,6 +180,16 @@ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTi
177
180
  const visibleRef = useRef(visible);
178
181
  visibleRef.current = visible;
179
182
  const [nativeVisible, setNativeVisible] = useState(visible);
183
+ // Entry focus waits for the native presentation (onShow) so it runs after the enter animation's interaction.
184
+ const [entered, setEntered] = useState(false);
185
+ // On the overlay stack from opening until the exit has fully torn down (where return focus runs), so screens below
186
+ // keep a pending entry focus until then.
187
+ const [layered, setLayered] = useState(visible);
188
+ useEffect(() => { if (!visible)
189
+ setEntered(false);
190
+ else
191
+ setLayered(true); }, [visible]);
192
+ const focusScope = useInitialFocusScope({ mode: initialFocus, active: visible && entered, layer: useOverlayLayer(layered) });
180
193
  const keyboardViewport = useModalKeyboardViewport(nativeVisible);
181
194
  const nativeVisibleRef = useRef(visible);
182
195
  nativeVisibleRef.current = nativeVisible;
@@ -251,8 +264,10 @@ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTi
251
264
  startEnter();
252
265
  return;
253
266
  }
254
- if (!visibleRef.current)
267
+ if (!visibleRef.current) {
268
+ setLayered(false);
255
269
  focusNativeTarget(returnFocusRef);
270
+ }
256
271
  }, [cancelExitFallback, motionProgress, returnFocusRef, startEnter]);
257
272
  const startExit = useCallback((token) => {
258
273
  measurementRun.current += 1;
@@ -361,66 +376,70 @@ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTi
361
376
  const contentBackground = resolveColorReference(dialogRecipe.content.background, palette);
362
377
  const contentBorder = resolveColorReference(dialogRecipe.content.border, palette);
363
378
  const stackActions = environment.textScale >= 1.6 || windowWidth < 480;
364
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
365
- nativeShownRef.current = false;
366
- const pending = pendingExitRef.current;
367
- if (pending)
368
- completeExit(pending.token);
369
- }, onRequestClose: () => requestClose("back"), onShow: (event) => {
370
- nativeShownRef.current = true;
371
- startEnter();
372
- onShow?.(event);
373
- }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
374
- flex: 1,
375
- justifyContent: "center",
376
- opacity: motionProgress,
377
- ...viewportPadding,
378
- paddingBottom: keyboardViewport.dockedKeyboard
379
- ? spacing.md + keyboardViewport.keyboardInset
380
- : viewportPadding.paddingBottom,
381
- }, children: [_jsx(Scrim, {}), dismissible ? (_jsx(Pressable, { accessible: false, importantForAccessibility: "no-hide-descendants", onPress: () => requestClose("outside"), style: { bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } })) : null, _jsxs(Animated.View, { ref: originContentRef, onLayout: (event) => {
382
- const { width, height } = event.nativeEvent.layout;
383
- const previous = originLayout.current;
384
- originLayout.current = { width, height };
385
- // Error copy, keyboard or rotation may change the destination after entry.
386
- // Fade instead of returning through stale dimensions; a later open remeasures.
387
- if (previous && (previous.width !== width || previous.height !== height))
388
- setOriginTransform(null);
389
- }, accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "dialog", style: [
390
- {
391
- alignSelf: "center",
392
- backgroundColor: contentBackground,
393
- borderColor: contentBorder,
394
- borderRadius: tokens.radius[dialogRecipe.content.radius],
395
- borderWidth: dialogRecipe.content.borderWidth,
396
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
397
- gap: dialogRecipe.content.gap,
398
- maxWidth: sizeRecipe.maxWidth,
399
- maxHeight: "100%",
400
- flexShrink: 1,
401
- padding: sizeRecipe.padding,
402
- shadowColor: surfaceShadow.color,
403
- shadowOffset: { width: 0, height: surfaceShadow.offsetY },
404
- shadowOpacity: surfaceShadow.opacity,
405
- shadowRadius: surfaceShadow.radius,
406
- width: "100%",
407
- },
408
- originTransform && !environment.reducedMotion ? { transform: [
409
- { translateX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateX, 0] }) },
410
- { translateY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateY, 0] }) },
411
- { scaleX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleX, 1] }) },
412
- { scaleY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleY, 1] }) },
413
- ] } : undefined,
414
- contentStyle,
415
- ], children: [_jsxs(View, { style: { flexShrink: 1, gap: description ? spacing.xs : dialogRecipe.content.gap }, children: [_jsxs(View, { style: {
416
- alignItems: "flex-start",
417
- direction: environment.direction,
418
- flexDirection: "row",
419
- gap: spacing.sm,
420
- }, children: [_jsx(View, { style: { flex: 1 }, children: _jsx(Text, { accessibilityRole: "header", tone: "primary", variant: "title", children: title }) }), dismissible ? (_jsx(IconButton, { disabled: busy, label: closeLabel, onPress: () => requestClose("close-action"), children: _jsx(CloseGlyph, {}) })) : null] }), description || children ? (_jsxs(DialogScrollBody, { gap: dialogRecipe.content.gap, children: [description ? _jsx(Text, { tone: "muted", children: description }) : null, _jsx(KeyboardAvoidanceContext.Provider, { value: true, children: children })] })) : null] }), _jsx(OverlayActions, { busy: busy, onAction: runAction, pendingAction: actionPending, stacked: stackActions, ...(primaryAction === undefined ? {} : { primaryAction }), ...(secondaryAction === undefined ? {} : { secondaryAction }) })] })] }) }) }));
379
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
380
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusProvider, { scope: focusScope, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
381
+ nativeShownRef.current = false;
382
+ const pending = pendingExitRef.current;
383
+ if (pending)
384
+ completeExit(pending.token);
385
+ }, onRequestClose: () => requestClose("back"), onShow: (event) => {
386
+ nativeShownRef.current = true;
387
+ startEnter();
388
+ setEntered(true);
389
+ onShow?.(event);
390
+ }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, onTouchStart: focusScope.onTouchStart, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
391
+ flex: 1,
392
+ justifyContent: "center",
393
+ opacity: motionProgress,
394
+ ...viewportPadding,
395
+ paddingBottom: keyboardViewport.dockedKeyboard
396
+ ? spacing.md + keyboardViewport.keyboardInset
397
+ : viewportPadding.paddingBottom,
398
+ }, children: [_jsx(Scrim, {}), dismissible ? (_jsx(Pressable, { accessible: false, importantForAccessibility: "no-hide-descendants", onPress: () => requestClose("outside"), style: { bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } })) : null, _jsxs(Animated.View, { ref: originContentRef, onLayout: (event) => {
399
+ const { width, height } = event.nativeEvent.layout;
400
+ const previous = originLayout.current;
401
+ originLayout.current = { width, height };
402
+ // Error copy, keyboard or rotation may change the destination after entry.
403
+ // Fade instead of returning through stale dimensions; a later open remeasures.
404
+ if (previous && (previous.width !== width || previous.height !== height))
405
+ setOriginTransform(null);
406
+ }, accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "dialog", style: [
407
+ {
408
+ alignSelf: "center",
409
+ backgroundColor: contentBackground,
410
+ borderColor: contentBorder,
411
+ borderRadius: tokens.radius[dialogRecipe.content.radius],
412
+ borderWidth: dialogRecipe.content.borderWidth,
413
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
414
+ gap: dialogRecipe.content.gap,
415
+ maxWidth: sizeRecipe.maxWidth,
416
+ maxHeight: "100%",
417
+ flexShrink: 1,
418
+ padding: sizeRecipe.padding,
419
+ shadowColor: surfaceShadow.color,
420
+ shadowOffset: { width: 0, height: surfaceShadow.offsetY },
421
+ shadowOpacity: surfaceShadow.opacity,
422
+ shadowRadius: surfaceShadow.radius,
423
+ width: "100%",
424
+ },
425
+ originTransform && !environment.reducedMotion ? { transform: [
426
+ { translateX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateX, 0] }) },
427
+ { translateY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateY, 0] }) },
428
+ { scaleX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleX, 1] }) },
429
+ { scaleY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleY, 1] }) },
430
+ ] } : undefined,
431
+ contentStyle,
432
+ ], children: [_jsxs(View, { style: { flexShrink: 1, gap: description ? spacing.xs : dialogRecipe.content.gap }, children: [_jsxs(View, { style: {
433
+ alignItems: "flex-start",
434
+ direction: environment.direction,
435
+ flexDirection: "row",
436
+ gap: spacing.sm,
437
+ }, children: [_jsx(View, { style: { flex: 1 }, children: _jsx(Text, { accessibilityRole: "header", tone: "primary", variant: "title", children: title }) }), dismissible ? (_jsx(IconButton, { disabled: busy, label: closeLabel, onPress: () => requestClose("close-action"), children: _jsx(CloseGlyph, {}) })) : null] }), description || children ? (_jsxs(DialogScrollBody, { gap: dialogRecipe.content.gap, children: [description ? _jsx(Text, { tone: "muted", children: description }) : null, _jsx(KeyboardAvoidanceContext.Provider, { value: true, children: children })] })) : null] }), _jsx(OverlayActions, { busy: busy, onAction: runAction, pendingAction: actionPending, stacked: stackActions, ...(primaryAction === undefined ? {} : { primaryAction }), ...(secondaryAction === undefined ? {} : { secondaryAction }) })] })] }) }) }) }) }));
421
438
  }
422
439
  /** Contract session owns duplicate confirms, busy dismissal, error and settlement. */
423
440
  export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = alertDialogRecipe.defaults.size, returnFocusRef, onResult, contentStyle, onShow, ...modalProps }) {
441
+ // Prompt-style content with one text entry is focused on open; confirm-only dialogs have no field to choose.
442
+ const initialFocus = initialFocusDefaults.overlay;
424
443
  validateAlertDialogRequest(request);
425
444
  const viewportPadding = useDialogViewportPadding();
426
445
  const { colors, environment, palette, tokens, designProfile } = useHjmNativeTheme();
@@ -441,6 +460,16 @@ export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = a
441
460
  const visibleRef = useRef(visible);
442
461
  visibleRef.current = visible;
443
462
  const [nativeVisible, setNativeVisible] = useState(visible);
463
+ // Entry focus waits for the native presentation (onShow) so it runs after the enter animation's interaction.
464
+ const [entered, setEntered] = useState(false);
465
+ // On the overlay stack from opening until the exit has fully torn down (where return focus runs), so screens below
466
+ // keep a pending entry focus until then.
467
+ const [layered, setLayered] = useState(visible);
468
+ useEffect(() => { if (!visible)
469
+ setEntered(false);
470
+ else
471
+ setLayered(true); }, [visible]);
472
+ const focusScope = useInitialFocusScope({ mode: initialFocus, active: visible && entered, layer: useOverlayLayer(layered) });
444
473
  const keyboardViewport = useModalKeyboardViewport(nativeVisible);
445
474
  const nativeVisibleRef = useRef(visible);
446
475
  nativeVisibleRef.current = nativeVisible;
@@ -499,8 +528,10 @@ export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = a
499
528
  startEnter();
500
529
  return;
501
530
  }
502
- if (!visibleRef.current)
531
+ if (!visibleRef.current) {
532
+ setLayered(false);
503
533
  focusNativeTarget(returnFocusRef);
534
+ }
504
535
  }, [cancelExitFallback, motionProgress, returnFocusRef, startEnter]);
505
536
  const startExit = useCallback((target) => {
506
537
  motionProgress.stopAnimation();
@@ -646,71 +677,72 @@ export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = a
646
677
  paddingHorizontal: spacing.md,
647
678
  },
648
679
  ], children: _jsx(Text, { style: { color: confirmContent }, variant: "label", children: request.confirmLabel }) }));
649
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
650
- nativeShownRef.current = false;
651
- const pending = pendingExitSessions.current[0];
652
- if (pending)
653
- completeExit(pending.session);
654
- }, onRequestClose: () => cancel("back"), onShow: focusLeastDestructive, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
655
- flex: 1,
656
- justifyContent: "center",
657
- opacity: motionProgress,
658
- ...viewportPadding,
659
- paddingBottom: keyboardViewport.dockedKeyboard
660
- ? spacing.md + keyboardViewport.keyboardInset
661
- : viewportPadding.paddingBottom,
662
- }, children: [_jsx(Scrim, {}), _jsxs(View, { accessibilityLabel: `${request.title}, ${request.description}`, accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "alertdialog", style: [
663
- {
664
- alignSelf: "center",
665
- backgroundColor: contentBackground,
666
- borderColor: contentBorder,
667
- borderRadius: tokens.radius[alertDialogRecipe.content.radius],
668
- borderWidth: alertDialogRecipe.content.borderWidth,
669
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
670
- gap: alertDialogRecipe.content.gap,
671
- maxWidth: sizeRecipe.maxWidth,
672
- maxHeight: "100%",
673
- flexShrink: 1,
674
- padding: sizeRecipe.padding,
675
- shadowColor: surfaceShadow.color,
676
- shadowOffset: {
677
- width: 0,
678
- height: surfaceShadow.offsetY,
679
- },
680
- shadowOpacity: surfaceShadow.opacity,
681
- shadowRadius: surfaceShadow.radius,
682
- width: "100%",
683
- },
684
- contentStyle,
685
- ], children: [_jsxs(DialogScrollBody, { gap: spacing.xs, children: [_jsx(Text, { accessibilityRole: "header", tone: "primary", variant: "title", children: request.title }), _jsx(Text, { tone: "muted", children: request.description }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", style: { color: errorColor }, children: error })) : null] }), _jsxs(View, { style: {
686
- direction: environment.direction,
687
- flexDirection: stackActions ? "column" : "row",
688
- gap: stackActions
689
- ? alertDialogRecipe.actions.stackedGap
690
- : alertDialogRecipe.actions.gap,
691
- }, children: [stackActions ? confirmAction : null, request.mode === "confirm" ? (_jsx(Pressable, { ref: cancelRef, accessibilityLabel: request.cancelLabel, accessibilityRole: "button", accessibilityState: { disabled: busy }, disabled: busy, onPress: () => cancel("cancel-action"), style: ({ pressed }) => [
692
- minimumTargetStyle,
693
- {
694
- alignItems: "center",
695
- // The cancel action is a quiet outline on the dialog surface. surfaceAlt
696
- // turned it into a tinted slab wherever a product brands that token
697
- // (BurnTok's pale blue, 2026-09-27), competing with the confirm fill.
698
- backgroundColor: colors.bg,
699
- borderColor: colors.border,
700
- borderRadius: tokens.radius.md,
701
- borderWidth: 1,
702
- flex: stackActions ? undefined : 1,
703
- justifyContent: "center",
704
- minWidth: alertDialogRecipe.actions.minButtonWidth,
705
- opacity: busy ? 0.5 : pressed ? 0.86 : 1,
706
- paddingHorizontal: spacing.md,
707
- },
708
- ], children: _jsx(Text, { variant: "label", children: request.cancelLabel }) })) : null, stackActions ? null : confirmAction] })] })] }) }) }));
680
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
681
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusProvider, { scope: focusScope, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
682
+ nativeShownRef.current = false;
683
+ const pending = pendingExitSessions.current[0];
684
+ if (pending)
685
+ completeExit(pending.session);
686
+ }, onRequestClose: () => cancel("back"), onShow: (event) => { focusLeastDestructive(event); setEntered(true); }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, onTouchStart: focusScope.onTouchStart, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
687
+ flex: 1,
688
+ justifyContent: "center",
689
+ opacity: motionProgress,
690
+ ...viewportPadding,
691
+ paddingBottom: keyboardViewport.dockedKeyboard
692
+ ? spacing.md + keyboardViewport.keyboardInset
693
+ : viewportPadding.paddingBottom,
694
+ }, children: [_jsx(Scrim, {}), _jsxs(View, { accessibilityLabel: `${request.title}, ${request.description}`, accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "alertdialog", style: [
695
+ {
696
+ alignSelf: "center",
697
+ backgroundColor: contentBackground,
698
+ borderColor: contentBorder,
699
+ borderRadius: tokens.radius[alertDialogRecipe.content.radius],
700
+ borderWidth: alertDialogRecipe.content.borderWidth,
701
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
702
+ gap: alertDialogRecipe.content.gap,
703
+ maxWidth: sizeRecipe.maxWidth,
704
+ maxHeight: "100%",
705
+ flexShrink: 1,
706
+ padding: sizeRecipe.padding,
707
+ shadowColor: surfaceShadow.color,
708
+ shadowOffset: {
709
+ width: 0,
710
+ height: surfaceShadow.offsetY,
711
+ },
712
+ shadowOpacity: surfaceShadow.opacity,
713
+ shadowRadius: surfaceShadow.radius,
714
+ width: "100%",
715
+ },
716
+ contentStyle,
717
+ ], children: [_jsxs(DialogScrollBody, { gap: spacing.xs, children: [_jsx(Text, { accessibilityRole: "header", tone: "primary", variant: "title", children: request.title }), _jsx(Text, { tone: "muted", children: request.description }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", style: { color: errorColor }, children: error })) : null] }), _jsxs(View, { style: {
718
+ direction: environment.direction,
719
+ flexDirection: stackActions ? "column" : "row",
720
+ gap: stackActions
721
+ ? alertDialogRecipe.actions.stackedGap
722
+ : alertDialogRecipe.actions.gap,
723
+ }, children: [stackActions ? confirmAction : null, request.mode === "confirm" ? (_jsx(Pressable, { ref: cancelRef, accessibilityLabel: request.cancelLabel, accessibilityRole: "button", accessibilityState: { disabled: busy }, disabled: busy, onPress: () => cancel("cancel-action"), style: ({ pressed }) => [
724
+ minimumTargetStyle,
725
+ {
726
+ alignItems: "center",
727
+ // The cancel action is a quiet outline on the dialog surface. surfaceAlt
728
+ // turned it into a tinted slab wherever a product brands that token
729
+ // (BurnTok's pale blue, 2026-09-27), competing with the confirm fill.
730
+ backgroundColor: colors.bg,
731
+ borderColor: colors.border,
732
+ borderRadius: tokens.radius.md,
733
+ borderWidth: 1,
734
+ flex: stackActions ? undefined : 1,
735
+ justifyContent: "center",
736
+ minWidth: alertDialogRecipe.actions.minButtonWidth,
737
+ opacity: busy ? 0.5 : pressed ? 0.86 : 1,
738
+ paddingHorizontal: spacing.md,
739
+ },
740
+ ], children: _jsx(Text, { variant: "label", children: request.cancelLabel }) })) : null, stackActions ? null : confirmAction] })] })] }) }) }) }) }));
709
741
  }
710
742
  /** Native Sheet applies policy before emitting a concrete dismissal reason. */
711
743
  export function Sheet({ open, defaultOpen, onOpenChange, title, accessibilityTitle, description, children, footer, placement = "bottom", size = sheetRecipe.defaults.size, busy = false, dismissPolicy, closeLabel, returnFocusRef, safeAreaInsets: suppliedSafeAreaInsets, onDismissComplete, contentStyle,
712
744
  // Default on since the 2026-10-16 keyboard audit: input sheets that forgot the opt-in were covered.
713
- keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }) {
745
+ keyboardAvoidance = true, scrollable = false, showClose, onShow, initialFocus = initialFocusDefaults.overlay, ...modalProps }) {
714
746
  const { environment, palette, tokens, designProfile } = useHjmNativeTheme();
715
747
  // The profile floating role is shared with Web. Modal lifecycle, inset and
716
748
  // action state remain recipe-owned; changing decoration must not replace them.
@@ -739,6 +771,16 @@ keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }
739
771
  const visibleRef = useRef(visible);
740
772
  visibleRef.current = visible;
741
773
  const [nativeVisible, setNativeVisible] = useState(visible);
774
+ // Entry focus waits for the native presentation (onShow) so it runs after the enter animation's interaction.
775
+ const [entered, setEntered] = useState(false);
776
+ // On the overlay stack from opening until the exit has fully torn down (where return focus runs), so screens below
777
+ // keep a pending entry focus until then.
778
+ const [layered, setLayered] = useState(visible);
779
+ useEffect(() => { if (!visible)
780
+ setEntered(false);
781
+ else
782
+ setLayered(true); }, [visible]);
783
+ const focusScope = useInitialFocusScope({ mode: initialFocus, active: visible && entered, layer: useOverlayLayer(layered) });
742
784
  const { viewportHeight, keyboardInset, dockedKeyboard, setModalHeight } = useModalKeyboardViewport(nativeVisible, keyboardAvoidance);
743
785
  const nativeVisibleRef = useRef(visible);
744
786
  nativeVisibleRef.current = nativeVisible;
@@ -822,6 +864,7 @@ keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }
822
864
  }
823
865
  if (!visibleRef.current) {
824
866
  onDismissComplete?.({ reason: pending.reason });
867
+ setLayered(false);
825
868
  focusNativeTarget(returnFocusRef);
826
869
  }
827
870
  }, [cancelDismissFallback, motionProgress, onDismissComplete, returnFocusRef]);
@@ -953,93 +996,95 @@ keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }
953
996
  const sheetBodyRoom = listRowScrollRoom(sheetRecipe.content.paddingHorizontal);
954
997
  const contentBackground = resolveColorReference(sheetRecipe.content.background, palette);
955
998
  const contentBorder = resolveColorReference(sheetRecipe.content.border, palette);
956
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
957
- nativeShownRef.current = false;
958
- const pending = pendingDismissals.current[0];
959
- if (pending)
960
- completeDismiss(pending.cycle);
961
- }, onRequestClose: () => requestClose("back"), onShow: (event) => {
962
- nativeShownRef.current = true;
963
- startEnter();
964
- onShow?.(event);
965
- }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => setModalHeight(event.nativeEvent.layout.height), style: {
966
- alignItems: physicalPlacement === "right" ? "flex-end" : "flex-start",
967
- flex: 1,
968
- justifyContent: physicalPlacement === "bottom" ? "flex-end" : "flex-start",
969
- opacity: motionProgress,
970
- paddingTop: insets.top,
971
- paddingBottom: keyboardInset,
972
- }, children: [_jsx(Scrim, {}), policy.dismissible && policy.outsideDismiss ? (
973
- // Backdrop press closes every sheet by default (2026-10-17 user decision) through the same
974
- // requestClose path as the handle, so busy and dismissPolicy guards apply. Named by closeLabel.
975
- _jsx(Pressable, { accessibilityLabel: closeLabel, accessibilityRole: "button", accessibilityState: { disabled: closeBlocked }, disabled: closeBlocked, onPress: () => requestClose("outside"), style: { bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } })) : null, _jsxs(Animated.View, { accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", onAccessibilityEscape: () => requestClose("escape"), onLayout: (event) => { sheetHeight.current = event.nativeEvent.layout.height; }, role: "dialog", style: [
976
- {
977
- backgroundColor: contentBackground,
978
- borderColor: contentBorder,
979
- borderRadius: tokens.radius[sheetRecipe.content.radius],
980
- borderBottomLeftRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
981
- borderBottomRightRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
982
- borderWidth: sheetRecipe.content.borderWidth,
983
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
984
- gap: sheetRecipe.body.gap,
985
- // A fixed size ratio drives the height; `auto` lets the content decide
986
- // and the recipe's maxHeightRatio caps it.
987
- height: side
988
- ? availableHeight
989
- : sizeRatio === null
990
- ? undefined
991
- : Math.min(viewportHeight * sizeRatio, availableHeight),
992
- maxWidth: side ? 420 : undefined,
993
- // maxHeightRatio caps only `auto`. A fixed size is already bounded by availableHeight
994
- // (viewport − top inset − keyboard); capping it too stopped `full` at 90% (2026-10-06 follow-up).
995
- maxHeight: side || sizeRatio !== null
996
- ? availableHeight
997
- : Math.min(availableHeight, viewportHeight * sheetRecipe.content.maxHeightRatio),
998
- paddingBottom: sheetRecipe.content.paddingBottom + bottomInset,
999
- paddingLeft: sheetRecipe.content.paddingHorizontal + insets.left,
1000
- paddingRight: sheetRecipe.content.paddingHorizontal + insets.right,
1001
- // The top safe area limits the viewport, rather than adding a second
1002
- // notch-sized gap inside a sheet anchored to the bottom.
1003
- paddingTop: sheetRecipe.content.paddingTop,
1004
- shadowColor: surfaceShadow.color,
1005
- shadowOffset: {
1006
- width: 0,
1007
- height: surfaceShadow.offsetY,
1008
- },
1009
- shadowOpacity: surfaceShadow.opacity,
1010
- shadowRadius: surfaceShadow.radius,
1011
- transform: environment.reducedMotion
1012
- ? undefined
1013
- : side
1014
- ? [{ translateX: translate }]
1015
- // Two translateY entries compose: the enter/exit motion plus the live drag offset.
1016
- : [{ translateY: translate }, { translateY: dragOffset }],
1017
- width: side ? "88%" : "100%",
1018
- },
1019
- contentStyle,
1020
- ], children: [_jsx(View, { accessible: true, accessibilityRole: "header", accessibilityLabel: accessibleTitle, style: { position: "absolute", top: 0, start: 0, width: 1, height: 1, overflow: "hidden" } }), _jsxs(View, { ...(canSwipe ? dragResponder.panHandlers : {}), style: { flexShrink: 0 }, children: [showHandle && sheetRecipe.handle.visibleByDefault ? (
1021
- // The handle doubles as a close control so switch and screen-reader users need no drag.
1022
- _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: closeLabel, accessibilityState: { disabled: closeBlocked }, disabled: closeBlocked, onPress: () => requestClose("close-action"), hitSlop: spacing.sm, style: { alignSelf: "center", paddingVertical: spacing.xs }, children: _jsx(View, { style: { width: sheetRecipe.handle.width, height: sheetRecipe.handle.height, borderRadius: tokens.radius[sheetRecipe.handle.radius], backgroundColor: resolveColorReference(sheetRecipe.handle.color, palette) } }) })) : null, description || (policy.dismissible && closeVisible) ? (_jsxs(View, { style: {
1023
- alignItems: "center",
1024
- minHeight: sheetRecipe.header.minHeight,
999
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
1000
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusProvider, { scope: focusScope, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
1001
+ nativeShownRef.current = false;
1002
+ const pending = pendingDismissals.current[0];
1003
+ if (pending)
1004
+ completeDismiss(pending.cycle);
1005
+ }, onRequestClose: () => requestClose("back"), onShow: (event) => {
1006
+ nativeShownRef.current = true;
1007
+ startEnter();
1008
+ setEntered(true);
1009
+ onShow?.(event);
1010
+ }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, onTouchStart: focusScope.onTouchStart, children: _jsxs(Animated.View, { onLayout: (event) => setModalHeight(event.nativeEvent.layout.height), style: {
1011
+ alignItems: physicalPlacement === "right" ? "flex-end" : "flex-start",
1012
+ flex: 1,
1013
+ justifyContent: physicalPlacement === "bottom" ? "flex-end" : "flex-start",
1014
+ opacity: motionProgress,
1015
+ paddingTop: insets.top,
1016
+ paddingBottom: keyboardInset,
1017
+ }, children: [_jsx(Scrim, {}), policy.dismissible && policy.outsideDismiss ? (
1018
+ // Backdrop press closes every sheet by default (2026-10-17 user decision) through the same
1019
+ // requestClose path as the handle, so busy and dismissPolicy guards apply. Named by closeLabel.
1020
+ _jsx(Pressable, { accessibilityLabel: closeLabel, accessibilityRole: "button", accessibilityState: { disabled: closeBlocked }, disabled: closeBlocked, onPress: () => requestClose("outside"), style: { bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } })) : null, _jsxs(Animated.View, { accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", onAccessibilityEscape: () => requestClose("escape"), onLayout: (event) => { sheetHeight.current = event.nativeEvent.layout.height; }, role: "dialog", style: [
1021
+ {
1022
+ backgroundColor: contentBackground,
1023
+ borderColor: contentBorder,
1024
+ borderRadius: tokens.radius[sheetRecipe.content.radius],
1025
+ borderBottomLeftRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
1026
+ borderBottomRightRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
1027
+ borderWidth: sheetRecipe.content.borderWidth,
1028
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
1029
+ gap: sheetRecipe.body.gap,
1030
+ // A fixed size ratio drives the height; `auto` lets the content decide
1031
+ // and the recipe's maxHeightRatio caps it.
1032
+ height: side
1033
+ ? availableHeight
1034
+ : sizeRatio === null
1035
+ ? undefined
1036
+ : Math.min(viewportHeight * sizeRatio, availableHeight),
1037
+ maxWidth: side ? 420 : undefined,
1038
+ // maxHeightRatio caps only `auto`. A fixed size is already bounded by availableHeight
1039
+ // (viewport − top inset − keyboard); capping it too stopped `full` at 90% (2026-10-06 follow-up).
1040
+ maxHeight: side || sizeRatio !== null
1041
+ ? availableHeight
1042
+ : Math.min(availableHeight, viewportHeight * sheetRecipe.content.maxHeightRatio),
1043
+ paddingBottom: sheetRecipe.content.paddingBottom + bottomInset,
1044
+ paddingLeft: sheetRecipe.content.paddingHorizontal + insets.left,
1045
+ paddingRight: sheetRecipe.content.paddingHorizontal + insets.right,
1046
+ // The top safe area limits the viewport, rather than adding a second
1047
+ // notch-sized gap inside a sheet anchored to the bottom.
1048
+ paddingTop: sheetRecipe.content.paddingTop,
1049
+ shadowColor: surfaceShadow.color,
1050
+ shadowOffset: {
1051
+ width: 0,
1052
+ height: surfaceShadow.offsetY,
1053
+ },
1054
+ shadowOpacity: surfaceShadow.opacity,
1055
+ shadowRadius: surfaceShadow.radius,
1056
+ transform: environment.reducedMotion
1057
+ ? undefined
1058
+ : side
1059
+ ? [{ translateX: translate }]
1060
+ // Two translateY entries compose: the enter/exit motion plus the live drag offset.
1061
+ : [{ translateY: translate }, { translateY: dragOffset }],
1062
+ width: side ? "88%" : "100%",
1063
+ },
1064
+ contentStyle,
1065
+ ], children: [_jsx(View, { accessible: true, accessibilityRole: "header", accessibilityLabel: accessibleTitle, style: { position: "absolute", top: 0, start: 0, width: 1, height: 1, overflow: "hidden" } }), _jsxs(View, { ...(canSwipe ? dragResponder.panHandlers : {}), style: { flexShrink: 0 }, children: [showHandle && sheetRecipe.handle.visibleByDefault ? (
1066
+ // The handle doubles as a close control so switch and screen-reader users need no drag.
1067
+ _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: closeLabel, accessibilityState: { disabled: closeBlocked }, disabled: closeBlocked, onPress: () => requestClose("close-action"), hitSlop: spacing.sm, style: { alignSelf: "center", paddingVertical: spacing.xs }, children: _jsx(View, { style: { width: sheetRecipe.handle.width, height: sheetRecipe.handle.height, borderRadius: tokens.radius[sheetRecipe.handle.radius], backgroundColor: resolveColorReference(sheetRecipe.handle.color, palette) } }) })) : null, description || (policy.dismissible && closeVisible) ? (_jsxs(View, { style: {
1068
+ alignItems: "center",
1069
+ minHeight: sheetRecipe.header.minHeight,
1070
+ flexShrink: 0,
1071
+ direction: environment.direction,
1072
+ flexDirection: "row",
1073
+ // Without a description the × keeps its inline-end place (2026-10-17: no visible title).
1074
+ justifyContent: "flex-end",
1075
+ gap: spacing.sm,
1076
+ }, children: [description ? _jsx(View, { style: { flex: 1 }, children: _jsx(Text, { tone: "muted", children: description }) }) : null, policy.dismissible && closeVisible ? (_jsx(IconButton, { disabled: busy && !policy.dismissWhileBusy, label: closeLabel, onPress: () => requestClose("close-action"), children: _jsx(CloseGlyph, {}) })) : null] })) : null] }), scrollable ? (_jsx(ScrollView
1077
+ // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1078
+ // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1079
+ // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1080
+ , {
1081
+ // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1082
+ // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1083
+ // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1084
+ style: { flexShrink: 1, minHeight: 0, ...bodyFill, ...sheetBodyRoom.viewport }, contentContainerStyle: { gap: sheetRecipe.body.gap, ...sheetBodyRoom.content }, keyboardShouldPersistTaps: "handled", keyboardDismissMode: Platform.OS === "ios" ? "interactive" : "on-drag", automaticallyAdjustKeyboardInsets: false, children: _jsx(KeyboardAvoidanceContext.Provider, { value: keyboardAvoidance, children: children }) })) : _jsx(View, { style: { gap: sheetRecipe.body.gap, flexShrink: 1, ...bodyFill }, children: _jsx(KeyboardAvoidanceContext.Provider, { value: keyboardAvoidance, children: children }) }), footer ? (_jsx(View, { style: {
1085
+ gap: sheetRecipe.footer.gap,
1025
1086
  flexShrink: 0,
1026
- direction: environment.direction,
1027
- flexDirection: "row",
1028
- // Without a description the × keeps its inline-end place (2026-10-17: no visible title).
1029
- justifyContent: "flex-end",
1030
- gap: spacing.sm,
1031
- }, children: [description ? _jsx(View, { style: { flex: 1 }, children: _jsx(Text, { tone: "muted", children: description }) }) : null, policy.dismissible && closeVisible ? (_jsx(IconButton, { disabled: busy && !policy.dismissWhileBusy, label: closeLabel, onPress: () => requestClose("close-action"), children: _jsx(CloseGlyph, {}) })) : null] })) : null] }), scrollable ? (_jsx(ScrollView
1032
- // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1033
- // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1034
- // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1035
- , {
1036
- // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1037
- // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1038
- // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1039
- style: { flexShrink: 1, minHeight: 0, ...bodyFill, ...sheetBodyRoom.viewport }, contentContainerStyle: { gap: sheetRecipe.body.gap, ...sheetBodyRoom.content }, keyboardShouldPersistTaps: "handled", keyboardDismissMode: Platform.OS === "ios" ? "interactive" : "on-drag", automaticallyAdjustKeyboardInsets: false, children: _jsx(KeyboardAvoidanceContext.Provider, { value: keyboardAvoidance, children: children }) })) : _jsx(View, { style: { gap: sheetRecipe.body.gap, flexShrink: 1, ...bodyFill }, children: _jsx(KeyboardAvoidanceContext.Provider, { value: keyboardAvoidance, children: children }) }), footer ? (_jsx(View, { style: {
1040
- gap: sheetRecipe.footer.gap,
1041
- flexShrink: 0,
1042
- paddingTop: sheetRecipe.footer.paddingTop,
1043
- }, children: footer })) : null] })] }) }) }));
1087
+ paddingTop: sheetRecipe.footer.paddingTop,
1088
+ }, children: footer })) : null] })] }) }) }) }) }));
1044
1089
  }
1045
1090
  //# sourceMappingURL=overlays.js.map