@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.
- package/dist/auth-screen.d.ts +19 -2
- package/dist/auth-screen.d.ts.map +1 -1
- package/dist/auth-screen.js +41 -24
- package/dist/auth-screen.js.map +1 -1
- package/dist/collection-rail.d.ts +7 -1
- package/dist/collection-rail.d.ts.map +1 -1
- package/dist/collection-rail.js +12 -5
- package/dist/collection-rail.js.map +1 -1
- package/dist/evidence.d.ts +316 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +52 -2
- package/dist/evidence.js.map +1 -1
- package/dist/feedback.d.ts +7 -1
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +4 -2
- package/dist/feedback.js.map +1 -1
- package/dist/inputs.d.ts +9 -0
- package/dist/inputs.d.ts.map +1 -1
- package/dist/inputs.js +4 -1
- package/dist/inputs.js.map +1 -1
- package/dist/internal/initial-focus.d.ts +44 -0
- package/dist/internal/initial-focus.d.ts.map +1 -0
- package/dist/internal/initial-focus.js +136 -0
- package/dist/internal/initial-focus.js.map +1 -0
- package/dist/internal/list-row-scroll-room.d.ts +2 -1
- package/dist/internal/list-row-scroll-room.d.ts.map +1 -1
- package/dist/internal/list-row-scroll-room.js +2 -1
- package/dist/internal/list-row-scroll-room.js.map +1 -1
- package/dist/internal/screen-body-inset.d.ts +9 -0
- package/dist/internal/screen-body-inset.d.ts.map +1 -0
- package/dist/internal/screen-body-inset.js +10 -0
- package/dist/internal/screen-body-inset.js.map +1 -0
- package/dist/keyboard.d.ts +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js.map +1 -1
- package/dist/overlays.d.ts +10 -2
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +255 -210
- package/dist/overlays.js.map +1 -1
- package/dist/screen-flows.d.ts +9 -4
- package/dist/screen-flows.d.ts.map +1 -1
- package/dist/screen-flows.js +12 -9
- package/dist/screen-flows.js.map +1 -1
- package/dist/screens.d.ts +26 -2
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +22 -6
- package/dist/screens.js.map +1 -1
- 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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
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
|