@hjmds/react-native 1.27.2 → 1.28.1

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 (95) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +9 -8
  3. package/dist/actions.js.map +1 -1
  4. package/dist/auth-screen.d.ts +41 -4
  5. package/dist/auth-screen.d.ts.map +1 -1
  6. package/dist/auth-screen.js +52 -8
  7. package/dist/auth-screen.js.map +1 -1
  8. package/dist/chart.d.ts +1 -1
  9. package/dist/chart.d.ts.map +1 -1
  10. package/dist/chart.js +8 -6
  11. package/dist/chart.js.map +1 -1
  12. package/dist/collection-rail.d.ts +7 -1
  13. package/dist/collection-rail.d.ts.map +1 -1
  14. package/dist/collection-rail.js +12 -5
  15. package/dist/collection-rail.js.map +1 -1
  16. package/dist/data-display.d.ts.map +1 -1
  17. package/dist/data-display.js +33 -10
  18. package/dist/data-display.js.map +1 -1
  19. package/dist/data-table.d.ts +2 -2
  20. package/dist/data-table.d.ts.map +1 -1
  21. package/dist/data-table.js +6 -5
  22. package/dist/data-table.js.map +1 -1
  23. package/dist/evidence.d.ts +1 -1
  24. package/dist/evidence.js +2 -2
  25. package/dist/evidence.js.map +1 -1
  26. package/dist/feedback.d.ts +12 -1
  27. package/dist/feedback.d.ts.map +1 -1
  28. package/dist/feedback.js +40 -18
  29. package/dist/feedback.js.map +1 -1
  30. package/dist/forms.d.ts.map +1 -1
  31. package/dist/forms.js +3 -1
  32. package/dist/forms.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/inputs.d.ts +6 -0
  38. package/dist/inputs.d.ts.map +1 -1
  39. package/dist/inputs.js +24 -6
  40. package/dist/inputs.js.map +1 -1
  41. package/dist/internal/icon-button-edge.d.ts +12 -0
  42. package/dist/internal/icon-button-edge.d.ts.map +1 -0
  43. package/dist/internal/icon-button-edge.js +20 -0
  44. package/dist/internal/icon-button-edge.js.map +1 -0
  45. package/dist/internal/initial-focus.d.ts +11 -0
  46. package/dist/internal/initial-focus.d.ts.map +1 -0
  47. package/dist/internal/initial-focus.js +60 -0
  48. package/dist/internal/initial-focus.js.map +1 -0
  49. package/dist/internal/list-row-scroll-room.d.ts +20 -0
  50. package/dist/internal/list-row-scroll-room.d.ts.map +1 -0
  51. package/dist/internal/list-row-scroll-room.js +19 -0
  52. package/dist/internal/list-row-scroll-room.js.map +1 -0
  53. package/dist/internal/message-reactions.d.ts +1 -1
  54. package/dist/internal/message-reactions.d.ts.map +1 -1
  55. package/dist/internal/screen-body-inset.d.ts +9 -0
  56. package/dist/internal/screen-body-inset.d.ts.map +1 -0
  57. package/dist/internal/screen-body-inset.js +10 -0
  58. package/dist/internal/screen-body-inset.js.map +1 -0
  59. package/dist/internal/screen-footer.d.ts +15 -2
  60. package/dist/internal/screen-footer.d.ts.map +1 -1
  61. package/dist/internal/screen-footer.js +7 -4
  62. package/dist/internal/screen-footer.js.map +1 -1
  63. package/dist/keyboard.d.ts +1 -1
  64. package/dist/keyboard.d.ts.map +1 -1
  65. package/dist/keyboard.js.map +1 -1
  66. package/dist/notification-bell.d.ts +4 -1
  67. package/dist/notification-bell.d.ts.map +1 -1
  68. package/dist/notification-bell.js +7 -2
  69. package/dist/notification-bell.js.map +1 -1
  70. package/dist/overlays.d.ts +10 -2
  71. package/dist/overlays.d.ts.map +1 -1
  72. package/dist/overlays.js +230 -193
  73. package/dist/overlays.js.map +1 -1
  74. package/dist/primitives.js +2 -2
  75. package/dist/primitives.js.map +1 -1
  76. package/dist/screen-flows.d.ts +11 -6
  77. package/dist/screen-flows.d.ts.map +1 -1
  78. package/dist/screen-flows.js +13 -10
  79. package/dist/screen-flows.js.map +1 -1
  80. package/dist/screens.d.ts +31 -3
  81. package/dist/screens.d.ts.map +1 -1
  82. package/dist/screens.js +27 -7
  83. package/dist/screens.js.map +1 -1
  84. package/dist/sheet-gesture.d.ts.map +1 -1
  85. package/dist/sheet-gesture.js +1 -2
  86. package/dist/sheet-gesture.js.map +1 -1
  87. package/dist/table-family.d.ts +6 -1
  88. package/dist/table-family.d.ts.map +1 -1
  89. package/dist/table-family.js +10 -10
  90. package/dist/table-family.js.map +1 -1
  91. package/dist/top.js +1 -1
  92. package/dist/virtual-list.d.ts.map +1 -1
  93. package/dist/virtual-list.js +5 -4
  94. package/dist/virtual-list.js.map +1 -1
  95. 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 { InitialFocusScope } 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";
@@ -14,6 +17,7 @@ import { Button, IconButton } from "./actions.js";
14
17
  import { scheduleAfterNativeModalTeardown, shouldAwaitNativeModalDismiss, } from "./internal/modal-lifecycle.js";
15
18
  import { isDevelopment } from "./internal/diagnostics.js";
16
19
  import { FixedGlyph } from "./internal/fixed-glyph.js";
20
+ import { listRowScrollRoom } from "./internal/list-row-scroll-room.js";
17
21
  import { minimumTargetStyle } from "./internal/styles.js";
18
22
  import { Text } from "./primitives.js";
19
23
  import { useHjmNativeSafeAreaInsets, useHjmNativeTheme } from "./provider.js";
@@ -148,7 +152,7 @@ function resolveOverlayAccessibleTitle(title, accessibilityTitle) {
148
152
  throw new TypeError("An element `title` requires `accessibilityTitle` for the modal accessible name");
149
153
  }
150
154
  /** Native modal boundary with one reasoned close intent for each user attempt. */
151
- 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 }) {
152
156
  const [actionPending, setActionPending] = useState(null);
153
157
  const actionRun = useRef(null);
154
158
  const busy = externalBusy || actionPending !== null;
@@ -176,6 +180,10 @@ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTi
176
180
  const visibleRef = useRef(visible);
177
181
  visibleRef.current = visible;
178
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
+ useEffect(() => { if (!visible)
186
+ setEntered(false); }, [visible]);
179
187
  const keyboardViewport = useModalKeyboardViewport(nativeVisible);
180
188
  const nativeVisibleRef = useRef(visible);
181
189
  nativeVisibleRef.current = nativeVisible;
@@ -360,66 +368,70 @@ export function Dialog({ open, defaultOpen, onOpenChange, title, accessibilityTi
360
368
  const contentBackground = resolveColorReference(dialogRecipe.content.background, palette);
361
369
  const contentBorder = resolveColorReference(dialogRecipe.content.border, palette);
362
370
  const stackActions = environment.textScale >= 1.6 || windowWidth < 480;
363
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
364
- nativeShownRef.current = false;
365
- const pending = pendingExitRef.current;
366
- if (pending)
367
- completeExit(pending.token);
368
- }, onRequestClose: () => requestClose("back"), onShow: (event) => {
369
- nativeShownRef.current = true;
370
- startEnter();
371
- onShow?.(event);
372
- }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
373
- flex: 1,
374
- justifyContent: "center",
375
- opacity: motionProgress,
376
- ...viewportPadding,
377
- paddingBottom: keyboardViewport.dockedKeyboard
378
- ? spacing.md + keyboardViewport.keyboardInset
379
- : viewportPadding.paddingBottom,
380
- }, 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) => {
381
- const { width, height } = event.nativeEvent.layout;
382
- const previous = originLayout.current;
383
- originLayout.current = { width, height };
384
- // Error copy, keyboard or rotation may change the destination after entry.
385
- // Fade instead of returning through stale dimensions; a later open remeasures.
386
- if (previous && (previous.width !== width || previous.height !== height))
387
- setOriginTransform(null);
388
- }, accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "dialog", style: [
389
- {
390
- alignSelf: "center",
391
- backgroundColor: contentBackground,
392
- borderColor: contentBorder,
393
- borderRadius: tokens.radius[dialogRecipe.content.radius],
394
- borderWidth: dialogRecipe.content.borderWidth,
395
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
396
- gap: dialogRecipe.content.gap,
397
- maxWidth: sizeRecipe.maxWidth,
398
- maxHeight: "100%",
399
- flexShrink: 1,
400
- padding: sizeRecipe.padding,
401
- shadowColor: surfaceShadow.color,
402
- shadowOffset: { width: 0, height: surfaceShadow.offsetY },
403
- shadowOpacity: surfaceShadow.opacity,
404
- shadowRadius: surfaceShadow.radius,
405
- width: "100%",
406
- },
407
- originTransform && !environment.reducedMotion ? { transform: [
408
- { translateX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateX, 0] }) },
409
- { translateY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateY, 0] }) },
410
- { scaleX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleX, 1] }) },
411
- { scaleY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleY, 1] }) },
412
- ] } : undefined,
413
- contentStyle,
414
- ], children: [_jsxs(View, { style: { flexShrink: 1, gap: description ? spacing.xs : dialogRecipe.content.gap }, children: [_jsxs(View, { style: {
415
- alignItems: "flex-start",
416
- direction: environment.direction,
417
- flexDirection: "row",
418
- gap: spacing.sm,
419
- }, 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 }) })] })] }) }) }));
371
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
372
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusScope, { mode: initialFocus, active: visible && entered, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
373
+ nativeShownRef.current = false;
374
+ const pending = pendingExitRef.current;
375
+ if (pending)
376
+ completeExit(pending.token);
377
+ }, onRequestClose: () => requestClose("back"), onShow: (event) => {
378
+ nativeShownRef.current = true;
379
+ startEnter();
380
+ setEntered(true);
381
+ onShow?.(event);
382
+ }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
383
+ flex: 1,
384
+ justifyContent: "center",
385
+ opacity: motionProgress,
386
+ ...viewportPadding,
387
+ paddingBottom: keyboardViewport.dockedKeyboard
388
+ ? spacing.md + keyboardViewport.keyboardInset
389
+ : viewportPadding.paddingBottom,
390
+ }, 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) => {
391
+ const { width, height } = event.nativeEvent.layout;
392
+ const previous = originLayout.current;
393
+ originLayout.current = { width, height };
394
+ // Error copy, keyboard or rotation may change the destination after entry.
395
+ // Fade instead of returning through stale dimensions; a later open remeasures.
396
+ if (previous && (previous.width !== width || previous.height !== height))
397
+ setOriginTransform(null);
398
+ }, accessibilityLabel: [accessibleTitle, description].filter(Boolean).join(", "), accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "dialog", style: [
399
+ {
400
+ alignSelf: "center",
401
+ backgroundColor: contentBackground,
402
+ borderColor: contentBorder,
403
+ borderRadius: tokens.radius[dialogRecipe.content.radius],
404
+ borderWidth: dialogRecipe.content.borderWidth,
405
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
406
+ gap: dialogRecipe.content.gap,
407
+ maxWidth: sizeRecipe.maxWidth,
408
+ maxHeight: "100%",
409
+ flexShrink: 1,
410
+ padding: sizeRecipe.padding,
411
+ shadowColor: surfaceShadow.color,
412
+ shadowOffset: { width: 0, height: surfaceShadow.offsetY },
413
+ shadowOpacity: surfaceShadow.opacity,
414
+ shadowRadius: surfaceShadow.radius,
415
+ width: "100%",
416
+ },
417
+ originTransform && !environment.reducedMotion ? { transform: [
418
+ { translateX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateX, 0] }) },
419
+ { translateY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.translateY, 0] }) },
420
+ { scaleX: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleX, 1] }) },
421
+ { scaleY: motionProgress.interpolate({ inputRange: [0, 1], outputRange: [originTransform.scaleY, 1] }) },
422
+ ] } : undefined,
423
+ contentStyle,
424
+ ], children: [_jsxs(View, { style: { flexShrink: 1, gap: description ? spacing.xs : dialogRecipe.content.gap }, children: [_jsxs(View, { style: {
425
+ alignItems: "flex-start",
426
+ direction: environment.direction,
427
+ flexDirection: "row",
428
+ gap: spacing.sm,
429
+ }, 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 }) })] })] }) }) }) }) }));
420
430
  }
421
431
  /** Contract session owns duplicate confirms, busy dismissal, error and settlement. */
422
432
  export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = alertDialogRecipe.defaults.size, returnFocusRef, onResult, contentStyle, onShow, ...modalProps }) {
433
+ // Prompt-style content with one text entry is focused on open; confirm-only dialogs have no field to choose.
434
+ const initialFocus = initialFocusDefaults.overlay;
423
435
  validateAlertDialogRequest(request);
424
436
  const viewportPadding = useDialogViewportPadding();
425
437
  const { colors, environment, palette, tokens, designProfile } = useHjmNativeTheme();
@@ -440,6 +452,10 @@ export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = a
440
452
  const visibleRef = useRef(visible);
441
453
  visibleRef.current = visible;
442
454
  const [nativeVisible, setNativeVisible] = useState(visible);
455
+ // Entry focus waits for the native presentation (onShow) so it runs after the enter animation's interaction.
456
+ const [entered, setEntered] = useState(false);
457
+ useEffect(() => { if (!visible)
458
+ setEntered(false); }, [visible]);
443
459
  const keyboardViewport = useModalKeyboardViewport(nativeVisible);
444
460
  const nativeVisibleRef = useRef(visible);
445
461
  nativeVisibleRef.current = nativeVisible;
@@ -645,71 +661,72 @@ export function AlertDialog({ open, defaultOpen, onOpenChange, request, size = a
645
661
  paddingHorizontal: spacing.md,
646
662
  },
647
663
  ], children: _jsx(Text, { style: { color: confirmContent }, variant: "label", children: request.confirmLabel }) }));
648
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
649
- nativeShownRef.current = false;
650
- const pending = pendingExitSessions.current[0];
651
- if (pending)
652
- completeExit(pending.session);
653
- }, 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: {
654
- flex: 1,
655
- justifyContent: "center",
656
- opacity: motionProgress,
657
- ...viewportPadding,
658
- paddingBottom: keyboardViewport.dockedKeyboard
659
- ? spacing.md + keyboardViewport.keyboardInset
660
- : viewportPadding.paddingBottom,
661
- }, children: [_jsx(Scrim, {}), _jsxs(View, { accessibilityLabel: `${request.title}, ${request.description}`, accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "alertdialog", style: [
662
- {
663
- alignSelf: "center",
664
- backgroundColor: contentBackground,
665
- borderColor: contentBorder,
666
- borderRadius: tokens.radius[alertDialogRecipe.content.radius],
667
- borderWidth: alertDialogRecipe.content.borderWidth,
668
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
669
- gap: alertDialogRecipe.content.gap,
670
- maxWidth: sizeRecipe.maxWidth,
671
- maxHeight: "100%",
672
- flexShrink: 1,
673
- padding: sizeRecipe.padding,
674
- shadowColor: surfaceShadow.color,
675
- shadowOffset: {
676
- width: 0,
677
- height: surfaceShadow.offsetY,
678
- },
679
- shadowOpacity: surfaceShadow.opacity,
680
- shadowRadius: surfaceShadow.radius,
681
- width: "100%",
682
- },
683
- contentStyle,
684
- ], 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: {
685
- direction: environment.direction,
686
- flexDirection: stackActions ? "column" : "row",
687
- gap: stackActions
688
- ? alertDialogRecipe.actions.stackedGap
689
- : alertDialogRecipe.actions.gap,
690
- }, 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 }) => [
691
- minimumTargetStyle,
692
- {
693
- alignItems: "center",
694
- // The cancel action is a quiet outline on the dialog surface. surfaceAlt
695
- // turned it into a tinted slab wherever a product brands that token
696
- // (BurnTok's pale blue, 2026-09-27), competing with the confirm fill.
697
- backgroundColor: colors.bg,
698
- borderColor: colors.border,
699
- borderRadius: tokens.radius.md,
700
- borderWidth: 1,
701
- flex: stackActions ? undefined : 1,
702
- justifyContent: "center",
703
- minWidth: alertDialogRecipe.actions.minButtonWidth,
704
- opacity: busy ? 0.5 : pressed ? 0.86 : 1,
705
- paddingHorizontal: spacing.md,
706
- },
707
- ], children: _jsx(Text, { variant: "label", children: request.cancelLabel }) })) : null, stackActions ? null : confirmAction] })] })] }) }) }));
664
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
665
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusScope, { mode: initialFocus, active: visible && entered, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
666
+ nativeShownRef.current = false;
667
+ const pending = pendingExitSessions.current[0];
668
+ if (pending)
669
+ completeExit(pending.session);
670
+ }, onRequestClose: () => cancel("back"), onShow: (event) => { focusLeastDestructive(event); setEntered(true); }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => keyboardViewport.setModalHeight(event.nativeEvent.layout.height), style: {
671
+ flex: 1,
672
+ justifyContent: "center",
673
+ opacity: motionProgress,
674
+ ...viewportPadding,
675
+ paddingBottom: keyboardViewport.dockedKeyboard
676
+ ? spacing.md + keyboardViewport.keyboardInset
677
+ : viewportPadding.paddingBottom,
678
+ }, children: [_jsx(Scrim, {}), _jsxs(View, { accessibilityLabel: `${request.title}, ${request.description}`, accessibilityState: { busy }, accessibilityViewIsModal: true, importantForAccessibility: "yes", role: "alertdialog", style: [
679
+ {
680
+ alignSelf: "center",
681
+ backgroundColor: contentBackground,
682
+ borderColor: contentBorder,
683
+ borderRadius: tokens.radius[alertDialogRecipe.content.radius],
684
+ borderWidth: alertDialogRecipe.content.borderWidth,
685
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
686
+ gap: alertDialogRecipe.content.gap,
687
+ maxWidth: sizeRecipe.maxWidth,
688
+ maxHeight: "100%",
689
+ flexShrink: 1,
690
+ padding: sizeRecipe.padding,
691
+ shadowColor: surfaceShadow.color,
692
+ shadowOffset: {
693
+ width: 0,
694
+ height: surfaceShadow.offsetY,
695
+ },
696
+ shadowOpacity: surfaceShadow.opacity,
697
+ shadowRadius: surfaceShadow.radius,
698
+ width: "100%",
699
+ },
700
+ contentStyle,
701
+ ], 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: {
702
+ direction: environment.direction,
703
+ flexDirection: stackActions ? "column" : "row",
704
+ gap: stackActions
705
+ ? alertDialogRecipe.actions.stackedGap
706
+ : alertDialogRecipe.actions.gap,
707
+ }, 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 }) => [
708
+ minimumTargetStyle,
709
+ {
710
+ alignItems: "center",
711
+ // The cancel action is a quiet outline on the dialog surface. surfaceAlt
712
+ // turned it into a tinted slab wherever a product brands that token
713
+ // (BurnTok's pale blue, 2026-09-27), competing with the confirm fill.
714
+ backgroundColor: colors.bg,
715
+ borderColor: colors.border,
716
+ borderRadius: tokens.radius.md,
717
+ borderWidth: 1,
718
+ flex: stackActions ? undefined : 1,
719
+ justifyContent: "center",
720
+ minWidth: alertDialogRecipe.actions.minButtonWidth,
721
+ opacity: busy ? 0.5 : pressed ? 0.86 : 1,
722
+ paddingHorizontal: spacing.md,
723
+ },
724
+ ], children: _jsx(Text, { variant: "label", children: request.cancelLabel }) })) : null, stackActions ? null : confirmAction] })] })] }) }) }) }) }));
708
725
  }
709
726
  /** Native Sheet applies policy before emitting a concrete dismissal reason. */
710
727
  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,
711
728
  // Default on since the 2026-10-16 keyboard audit: input sheets that forgot the opt-in were covered.
712
- keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }) {
729
+ keyboardAvoidance = true, scrollable = false, showClose, onShow, initialFocus = initialFocusDefaults.overlay, ...modalProps }) {
713
730
  const { environment, palette, tokens, designProfile } = useHjmNativeTheme();
714
731
  // The profile floating role is shared with Web. Modal lifecycle, inset and
715
732
  // action state remain recipe-owned; changing decoration must not replace them.
@@ -738,6 +755,10 @@ keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }
738
755
  const visibleRef = useRef(visible);
739
756
  visibleRef.current = visible;
740
757
  const [nativeVisible, setNativeVisible] = useState(visible);
758
+ // Entry focus waits for the native presentation (onShow) so it runs after the enter animation's interaction.
759
+ const [entered, setEntered] = useState(false);
760
+ useEffect(() => { if (!visible)
761
+ setEntered(false); }, [visible]);
741
762
  const { viewportHeight, keyboardInset, dockedKeyboard, setModalHeight } = useModalKeyboardViewport(nativeVisible, keyboardAvoidance);
742
763
  const nativeVisibleRef = useRef(visible);
743
764
  nativeVisibleRef.current = nativeVisible;
@@ -949,82 +970,98 @@ keyboardAvoidance = true, scrollable = false, showClose, onShow, ...modalProps }
949
970
  // `auto` stays content-sized: growing there has no free space to take. The ScrollView body grows too, so the
950
971
  // footer sits at the bottom on both platforms; its content container does not, so scroll content keeps its height.
951
972
  const bodyFill = side || sizeRatio !== null ? { flexGrow: 1, minHeight: 0 } : {};
973
+ const sheetBodyRoom = listRowScrollRoom(sheetRecipe.content.paddingHorizontal);
952
974
  const contentBackground = resolveColorReference(sheetRecipe.content.background, palette);
953
975
  const contentBorder = resolveColorReference(sheetRecipe.content.border, palette);
954
- return (_jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
955
- nativeShownRef.current = false;
956
- const pending = pendingDismissals.current[0];
957
- if (pending)
958
- completeDismiss(pending.cycle);
959
- }, onRequestClose: () => requestClose("back"), onShow: (event) => {
960
- nativeShownRef.current = true;
961
- startEnter();
962
- onShow?.(event);
963
- }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => setModalHeight(event.nativeEvent.layout.height), style: {
964
- alignItems: physicalPlacement === "right" ? "flex-end" : "flex-start",
965
- flex: 1,
966
- justifyContent: physicalPlacement === "bottom" ? "flex-end" : "flex-start",
967
- opacity: motionProgress,
968
- paddingTop: insets.top,
969
- paddingBottom: keyboardInset,
970
- }, children: [_jsx(Scrim, {}), policy.dismissible && policy.outsideDismiss ? (_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, { 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: [
971
- {
972
- backgroundColor: contentBackground,
973
- borderColor: contentBorder,
974
- borderRadius: tokens.radius[sheetRecipe.content.radius],
975
- borderBottomLeftRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
976
- borderBottomRightRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
977
- borderWidth: sheetRecipe.content.borderWidth,
978
- ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
979
- gap: sheetRecipe.body.gap,
980
- // A fixed size ratio drives the height; `auto` lets the content decide
981
- // and the recipe's maxHeightRatio caps it.
982
- height: side
983
- ? availableHeight
984
- : sizeRatio === null
985
- ? undefined
986
- : Math.min(viewportHeight * sizeRatio, availableHeight),
987
- maxWidth: side ? 420 : undefined,
988
- // maxHeightRatio caps only `auto`. A fixed size is already bounded by availableHeight
989
- // (viewport − top inset − keyboard); capping it too stopped `full` at 90% (2026-10-06 follow-up).
990
- maxHeight: side || sizeRatio !== null
991
- ? availableHeight
992
- : Math.min(availableHeight, viewportHeight * sheetRecipe.content.maxHeightRatio),
993
- paddingBottom: sheetRecipe.content.paddingBottom + bottomInset,
994
- paddingLeft: sheetRecipe.content.paddingHorizontal + insets.left,
995
- paddingRight: sheetRecipe.content.paddingHorizontal + insets.right,
996
- // The top safe area limits the viewport, rather than adding a second
997
- // notch-sized gap inside a sheet anchored to the bottom.
998
- paddingTop: sheetRecipe.content.paddingTop,
999
- shadowColor: surfaceShadow.color,
1000
- shadowOffset: {
1001
- width: 0,
1002
- height: surfaceShadow.offsetY,
1003
- },
1004
- shadowOpacity: surfaceShadow.opacity,
1005
- shadowRadius: surfaceShadow.radius,
1006
- transform: environment.reducedMotion
1007
- ? undefined
1008
- : side
1009
- ? [{ translateX: translate }]
1010
- // Two translateY entries compose: the enter/exit motion plus the live drag offset.
1011
- : [{ translateY: translate }, { translateY: dragOffset }],
1012
- width: side ? "88%" : "100%",
1013
- },
1014
- contentStyle,
1015
- ], children: [_jsxs(View, { ...(canSwipe ? dragResponder.panHandlers : {}), style: { flexShrink: 0 }, children: [showHandle && sheetRecipe.handle.visibleByDefault ? (
1016
- // The handle doubles as a close control so switch and screen-reader users need no drag.
1017
- _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, _jsxs(View, { style: {
1018
- alignItems: "center",
1019
- minHeight: sheetRecipe.header.minHeight,
976
+ // Overlay content is not in the screen body: a bleeding rail inside must not reach for the screen gutter.
977
+ return (_jsx(ScreenBodyInsetContext.Provider, { value: 0, children: _jsx(InitialFocusScope, { mode: initialFocus, active: visible && entered, children: _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: () => {
978
+ nativeShownRef.current = false;
979
+ const pending = pendingDismissals.current[0];
980
+ if (pending)
981
+ completeDismiss(pending.cycle);
982
+ }, onRequestClose: () => requestClose("back"), onShow: (event) => {
983
+ nativeShownRef.current = true;
984
+ startEnter();
985
+ setEntered(true);
986
+ onShow?.(event);
987
+ }, statusBarTranslucent: true, transparent: true, visible: nativeVisible, children: _jsx(ModalInteractionBoundary, { visible: visible, children: _jsxs(Animated.View, { onLayout: (event) => setModalHeight(event.nativeEvent.layout.height), style: {
988
+ alignItems: physicalPlacement === "right" ? "flex-end" : "flex-start",
989
+ flex: 1,
990
+ justifyContent: physicalPlacement === "bottom" ? "flex-end" : "flex-start",
991
+ opacity: motionProgress,
992
+ paddingTop: insets.top,
993
+ paddingBottom: keyboardInset,
994
+ }, children: [_jsx(Scrim, {}), policy.dismissible && policy.outsideDismiss ? (
995
+ // Backdrop press closes every sheet by default (2026-10-17 user decision) through the same
996
+ // requestClose path as the handle, so busy and dismissPolicy guards apply. Named by closeLabel.
997
+ _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: [
998
+ {
999
+ backgroundColor: contentBackground,
1000
+ borderColor: contentBorder,
1001
+ borderRadius: tokens.radius[sheetRecipe.content.radius],
1002
+ borderBottomLeftRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
1003
+ borderBottomRightRadius: physicalPlacement === "bottom" ? 0 : tokens.radius[sheetRecipe.content.radius],
1004
+ borderWidth: sheetRecipe.content.borderWidth,
1005
+ ...resolveNativeShadowElevation(surfaceShadow, designProfile !== undefined, 8),
1006
+ gap: sheetRecipe.body.gap,
1007
+ // A fixed size ratio drives the height; `auto` lets the content decide
1008
+ // and the recipe's maxHeightRatio caps it.
1009
+ height: side
1010
+ ? availableHeight
1011
+ : sizeRatio === null
1012
+ ? undefined
1013
+ : Math.min(viewportHeight * sizeRatio, availableHeight),
1014
+ maxWidth: side ? 420 : undefined,
1015
+ // maxHeightRatio caps only `auto`. A fixed size is already bounded by availableHeight
1016
+ // (viewport − top inset − keyboard); capping it too stopped `full` at 90% (2026-10-06 follow-up).
1017
+ maxHeight: side || sizeRatio !== null
1018
+ ? availableHeight
1019
+ : Math.min(availableHeight, viewportHeight * sheetRecipe.content.maxHeightRatio),
1020
+ paddingBottom: sheetRecipe.content.paddingBottom + bottomInset,
1021
+ paddingLeft: sheetRecipe.content.paddingHorizontal + insets.left,
1022
+ paddingRight: sheetRecipe.content.paddingHorizontal + insets.right,
1023
+ // The top safe area limits the viewport, rather than adding a second
1024
+ // notch-sized gap inside a sheet anchored to the bottom.
1025
+ paddingTop: sheetRecipe.content.paddingTop,
1026
+ shadowColor: surfaceShadow.color,
1027
+ shadowOffset: {
1028
+ width: 0,
1029
+ height: surfaceShadow.offsetY,
1030
+ },
1031
+ shadowOpacity: surfaceShadow.opacity,
1032
+ shadowRadius: surfaceShadow.radius,
1033
+ transform: environment.reducedMotion
1034
+ ? undefined
1035
+ : side
1036
+ ? [{ translateX: translate }]
1037
+ // Two translateY entries compose: the enter/exit motion plus the live drag offset.
1038
+ : [{ translateY: translate }, { translateY: dragOffset }],
1039
+ width: side ? "88%" : "100%",
1040
+ },
1041
+ contentStyle,
1042
+ ], 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 ? (
1043
+ // The handle doubles as a close control so switch and screen-reader users need no drag.
1044
+ _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: {
1045
+ alignItems: "center",
1046
+ minHeight: sheetRecipe.header.minHeight,
1047
+ flexShrink: 0,
1048
+ direction: environment.direction,
1049
+ flexDirection: "row",
1050
+ // Without a description the × keeps its inline-end place (2026-10-17: no visible title).
1051
+ justifyContent: "flex-end",
1052
+ gap: spacing.sm,
1053
+ }, 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
1054
+ // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1055
+ // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1056
+ // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1057
+ , {
1058
+ // The sheet's inline padding used to sit outside this clipping viewport, so ListRows cut their surface
1059
+ // bleed. The viewport now reaches the padding edge and carries the padding inside its content (Web's
1060
+ // body scroll box does the same); content keeps its position (2026-10-17 1.27.2 adoption).
1061
+ 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: {
1062
+ gap: sheetRecipe.footer.gap,
1020
1063
  flexShrink: 0,
1021
- direction: environment.direction,
1022
- flexDirection: "row",
1023
- gap: spacing.sm,
1024
- }, children: [_jsxs(View, { style: { flex: 1, gap: spacing.xs }, children: [_jsx(Text, { accessibilityRole: "header", tone: "primary", variant: "title", children: title }), description ? _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] })] }), scrollable ? (_jsx(ScrollView, { style: { flexShrink: 1, minHeight: 0, ...bodyFill }, contentContainerStyle: { gap: sheetRecipe.body.gap }, 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: {
1025
- gap: sheetRecipe.footer.gap,
1026
- flexShrink: 0,
1027
- paddingTop: sheetRecipe.footer.paddingTop,
1028
- }, children: footer })) : null] })] }) }) }));
1064
+ paddingTop: sheetRecipe.footer.paddingTop,
1065
+ }, children: footer })) : null] })] }) }) }) }) }));
1029
1066
  }
1030
1067
  //# sourceMappingURL=overlays.js.map