@oxy.so/bloom 4.17.0 → 4.18.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.
- package/docs/adoption-matrix.mdx +7 -7
- package/docs/breadcrumb.mdx +11 -0
- package/docs/cart-panel.mdx +4 -2
- package/docs/checkout-summary.mdx +4 -0
- package/docs/chip.mdx +14 -1
- package/docs/dialog.mdx +5 -4
- package/docs/loading.mdx +11 -0
- package/docs/overlay.mdx +21 -0
- package/docs/price-breakdown.mdx +17 -0
- package/docs/rating.mdx +22 -0
- package/docs/select.mdx +10 -0
- package/docs/stepper.mdx +25 -0
- package/lib/commonjs/breadcrumb/Breadcrumb.js +36 -6
- package/lib/commonjs/breadcrumb/Breadcrumb.js.map +1 -1
- package/lib/commonjs/cart-panel/CartLine.js +32 -10
- package/lib/commonjs/cart-panel/CartLine.js.map +1 -1
- package/lib/commonjs/cart-panel/CartPanel.js +3 -0
- package/lib/commonjs/cart-panel/CartPanel.js.map +1 -1
- package/lib/commonjs/cart-panel/shared.js +2 -0
- package/lib/commonjs/cart-panel/shared.js.map +1 -1
- package/lib/commonjs/chat-list/parts.js +1 -1
- package/lib/commonjs/chat-list/parts.js.map +1 -1
- package/lib/commonjs/chat-screen/ChatBackground.js +6 -6
- package/lib/commonjs/chat-screen/ChatBackground.js.map +1 -1
- package/lib/commonjs/checkout-summary/CheckoutConfirm.js +11 -1
- package/lib/commonjs/checkout-summary/CheckoutConfirm.js.map +1 -1
- package/lib/commonjs/chip/Chip.js +3 -1
- package/lib/commonjs/chip/Chip.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +20 -6
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +11 -5
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/placement.js +18 -1
- package/lib/commonjs/dialog/placement.js.map +1 -1
- package/lib/commonjs/loading/Loading.js +7 -3
- package/lib/commonjs/loading/Loading.js.map +1 -1
- package/lib/commonjs/loading/Loading.web.js +7 -3
- package/lib/commonjs/loading/Loading.web.js.map +1 -1
- package/lib/commonjs/loading/LoadingBase.js +49 -2
- package/lib/commonjs/loading/LoadingBase.js.map +1 -1
- package/lib/commonjs/location-puck/LocationPuck.js +1 -1
- package/lib/commonjs/location-puck/LocationPuck.js.map +1 -1
- package/lib/commonjs/mail-list/parts.js +1 -1
- package/lib/commonjs/mail-list/parts.js.map +1 -1
- package/lib/commonjs/media-card/parts.js +1 -1
- package/lib/commonjs/media-card/parts.js.map +1 -1
- package/lib/commonjs/overlay/Overlay.js +19 -4
- package/lib/commonjs/overlay/Overlay.js.map +1 -1
- package/lib/commonjs/overlay/use-hardware-back.js +32 -0
- package/lib/commonjs/overlay/use-hardware-back.js.map +1 -0
- package/lib/commonjs/price-breakdown/PriceSummary.js +11 -1
- package/lib/commonjs/price-breakdown/PriceSummary.js.map +1 -1
- package/lib/commonjs/price-breakdown/PriceSummaryLine.js +13 -1
- package/lib/commonjs/price-breakdown/PriceSummaryLine.js.map +1 -1
- package/lib/commonjs/rating/Rating.js +90 -4
- package/lib/commonjs/rating/Rating.js.map +1 -1
- package/lib/commonjs/search/Search.js +36 -3
- package/lib/commonjs/search/Search.js.map +1 -1
- package/lib/commonjs/settings-modal/SettingsModal.js +19 -12
- package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
- package/lib/commonjs/stepper/Stepper.js +18 -5
- package/lib/commonjs/stepper/Stepper.js.map +1 -1
- package/lib/commonjs/teleport/PortalHost.js +1 -1
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
- package/lib/module/breadcrumb/Breadcrumb.js +36 -6
- package/lib/module/breadcrumb/Breadcrumb.js.map +1 -1
- package/lib/module/cart-panel/CartLine.js +32 -10
- package/lib/module/cart-panel/CartLine.js.map +1 -1
- package/lib/module/cart-panel/CartPanel.js +3 -0
- package/lib/module/cart-panel/CartPanel.js.map +1 -1
- package/lib/module/cart-panel/shared.js +2 -0
- package/lib/module/cart-panel/shared.js.map +1 -1
- package/lib/module/chat-list/parts.js +1 -1
- package/lib/module/chat-list/parts.js.map +1 -1
- package/lib/module/chat-screen/ChatBackground.js +6 -6
- package/lib/module/chat-screen/ChatBackground.js.map +1 -1
- package/lib/module/checkout-summary/CheckoutConfirm.js +11 -1
- package/lib/module/checkout-summary/CheckoutConfirm.js.map +1 -1
- package/lib/module/chip/Chip.js +3 -1
- package/lib/module/chip/Chip.js.map +1 -1
- package/lib/module/dialog/Dialog.js +21 -7
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +12 -6
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/placement.js +17 -1
- package/lib/module/dialog/placement.js.map +1 -1
- package/lib/module/loading/Loading.js +8 -4
- package/lib/module/loading/Loading.js.map +1 -1
- package/lib/module/loading/Loading.web.js +8 -4
- package/lib/module/loading/Loading.web.js.map +1 -1
- package/lib/module/loading/LoadingBase.js +49 -3
- package/lib/module/loading/LoadingBase.js.map +1 -1
- package/lib/module/location-puck/LocationPuck.js +1 -1
- package/lib/module/location-puck/LocationPuck.js.map +1 -1
- package/lib/module/mail-list/parts.js +1 -1
- package/lib/module/mail-list/parts.js.map +1 -1
- package/lib/module/media-card/parts.js +1 -1
- package/lib/module/media-card/parts.js.map +1 -1
- package/lib/module/overlay/Overlay.js +19 -4
- package/lib/module/overlay/Overlay.js.map +1 -1
- package/lib/module/overlay/use-hardware-back.js +29 -0
- package/lib/module/overlay/use-hardware-back.js.map +1 -0
- package/lib/module/price-breakdown/PriceSummary.js +11 -1
- package/lib/module/price-breakdown/PriceSummary.js.map +1 -1
- package/lib/module/price-breakdown/PriceSummaryLine.js +13 -1
- package/lib/module/price-breakdown/PriceSummaryLine.js.map +1 -1
- package/lib/module/rating/Rating.js +89 -4
- package/lib/module/rating/Rating.js.map +1 -1
- package/lib/module/search/Search.js +37 -4
- package/lib/module/search/Search.js.map +1 -1
- package/lib/module/settings-modal/SettingsModal.js +20 -13
- package/lib/module/settings-modal/SettingsModal.js.map +1 -1
- package/lib/module/stepper/Stepper.js +18 -5
- package/lib/module/stepper/Stepper.js.map +1 -1
- package/lib/module/teleport/PortalHost.js +1 -1
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
- package/lib/typescript/commonjs/breadcrumb/Breadcrumb.d.ts.map +1 -1
- package/lib/typescript/commonjs/breadcrumb/types.d.ts +7 -0
- package/lib/typescript/commonjs/breadcrumb/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/CartLine.d.ts +10 -5
- package/lib/typescript/commonjs/cart-panel/CartLine.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts +1 -1
- package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/shared.d.ts +1 -1
- package/lib/typescript/commonjs/cart-panel/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/cart-panel/types.d.ts +18 -0
- package/lib/typescript/commonjs/cart-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts +4 -1
- package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkout-summary/types.d.ts +6 -0
- package/lib/typescript/commonjs/checkout-summary/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/commonjs/chip/types.d.ts +8 -6
- package/lib/typescript/commonjs/chip/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/placement.d.ts +13 -2
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +3 -2
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/LoadingBase.d.ts +18 -0
- package/lib/typescript/commonjs/loading/LoadingBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/types.d.ts +11 -0
- package/lib/typescript/commonjs/loading/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/types.d.ts +18 -0
- package/lib/typescript/commonjs/overlay/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts +11 -0
- package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts.map +1 -0
- package/lib/typescript/commonjs/price-breakdown/PriceSummary.d.ts.map +1 -1
- package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts +3 -1
- package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
- package/lib/typescript/commonjs/price-breakdown/types.d.ts +12 -0
- package/lib/typescript/commonjs/price-breakdown/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts +3 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/index.d.ts +1 -1
- package/lib/typescript/commonjs/rating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +20 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/search/Search.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/types.d.ts +7 -0
- package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/types.d.ts +9 -0
- package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/teleport/PortalHost.d.ts +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
- package/lib/typescript/module/breadcrumb/Breadcrumb.d.ts.map +1 -1
- package/lib/typescript/module/breadcrumb/types.d.ts +7 -0
- package/lib/typescript/module/breadcrumb/types.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/CartLine.d.ts +10 -5
- package/lib/typescript/module/cart-panel/CartLine.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/CartPanel.d.ts +1 -1
- package/lib/typescript/module/cart-panel/CartPanel.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/shared.d.ts +1 -1
- package/lib/typescript/module/cart-panel/shared.d.ts.map +1 -1
- package/lib/typescript/module/cart-panel/types.d.ts +18 -0
- package/lib/typescript/module/cart-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts +4 -1
- package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
- package/lib/typescript/module/checkout-summary/types.d.ts +6 -0
- package/lib/typescript/module/checkout-summary/types.d.ts.map +1 -1
- package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/module/chip/types.d.ts +8 -6
- package/lib/typescript/module/chip/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/placement.d.ts +13 -2
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +3 -2
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/LoadingBase.d.ts +18 -0
- package/lib/typescript/module/loading/LoadingBase.d.ts.map +1 -1
- package/lib/typescript/module/loading/types.d.ts +11 -0
- package/lib/typescript/module/loading/types.d.ts.map +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/module/overlay/types.d.ts +18 -0
- package/lib/typescript/module/overlay/types.d.ts.map +1 -1
- package/lib/typescript/module/overlay/use-hardware-back.d.ts +11 -0
- package/lib/typescript/module/overlay/use-hardware-back.d.ts.map +1 -0
- package/lib/typescript/module/price-breakdown/PriceSummary.d.ts.map +1 -1
- package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts +3 -1
- package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
- package/lib/typescript/module/price-breakdown/types.d.ts +12 -0
- package/lib/typescript/module/price-breakdown/types.d.ts.map +1 -1
- package/lib/typescript/module/rating/Rating.d.ts +3 -1
- package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/module/rating/index.d.ts +1 -1
- package/lib/typescript/module/rating/index.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +20 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/search/Search.d.ts.map +1 -1
- package/lib/typescript/module/select/types.d.ts +7 -0
- package/lib/typescript/module/select/types.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/module/stepper/types.d.ts +9 -0
- package/lib/typescript/module/stepper/types.d.ts.map +1 -1
- package/lib/typescript/module/teleport/PortalHost.d.ts +1 -1
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/breadcrumb/Breadcrumb.tsx +29 -4
- package/src/breadcrumb/types.ts +7 -0
- package/src/cart-panel/CartLine.tsx +30 -7
- package/src/cart-panel/CartPanel.tsx +3 -0
- package/src/cart-panel/shared.ts +11 -1
- package/src/cart-panel/types.ts +18 -0
- package/src/chat-list/parts.tsx +1 -1
- package/src/chat-screen/ChatBackground.tsx +6 -6
- package/src/checkout-summary/CheckoutConfirm.tsx +9 -0
- package/src/checkout-summary/types.ts +6 -0
- package/src/chip/Chip.tsx +3 -1
- package/src/chip/types.ts +8 -6
- package/src/dialog/Dialog.tsx +28 -9
- package/src/dialog/Dialog.web.tsx +15 -7
- package/src/dialog/placement.ts +19 -2
- package/src/dialog/types.ts +3 -2
- package/src/loading/Loading.tsx +6 -2
- package/src/loading/Loading.web.tsx +6 -2
- package/src/loading/LoadingBase.tsx +55 -5
- package/src/loading/types.ts +11 -0
- package/src/location-puck/LocationPuck.tsx +1 -1
- package/src/mail-list/parts.tsx +1 -1
- package/src/media-card/parts.tsx +1 -1
- package/src/overlay/Overlay.tsx +32 -14
- package/src/overlay/types.ts +18 -0
- package/src/overlay/use-hardware-back.ts +27 -0
- package/src/price-breakdown/PriceSummary.tsx +10 -0
- package/src/price-breakdown/PriceSummaryLine.tsx +12 -0
- package/src/price-breakdown/types.ts +12 -0
- package/src/rating/Rating.tsx +87 -3
- package/src/rating/index.ts +1 -0
- package/src/rating/types.ts +21 -0
- package/src/search/Search.tsx +42 -4
- package/src/select/types.ts +7 -0
- package/src/settings-modal/SettingsModal.tsx +20 -15
- package/src/stepper/Stepper.tsx +18 -5
- package/src/stepper/types.ts +9 -0
- package/src/teleport/PortalHost.tsx +1 -1
- package/src/zoomable-media-gallery/ZoomableMediaGallery.tsx +33 -12
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import Animated, {
|
|
|
28
28
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
29
29
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
30
30
|
import { useTheme } from '../theme/use-theme';
|
|
31
|
+
import { useIsRtl } from '../hooks/use-is-rtl';
|
|
31
32
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
32
33
|
import { StyledView } from '../styles/styled-primitives';
|
|
33
34
|
import { SurfaceLevelProvider } from '../styles/surface-levels';
|
|
@@ -51,6 +52,8 @@ import {
|
|
|
51
52
|
PANEL_RADIUS,
|
|
52
53
|
SIDE_SHEET_MIN_GUTTER,
|
|
53
54
|
useResolvedPlacement,
|
|
55
|
+
physicalDialogSide,
|
|
56
|
+
type DialogSidePlacement,
|
|
54
57
|
} from './placement';
|
|
55
58
|
import type {
|
|
56
59
|
DialogAction,
|
|
@@ -137,9 +140,9 @@ function CenteredOrSideDialog({
|
|
|
137
140
|
containerClassName,
|
|
138
141
|
label,
|
|
139
142
|
children,
|
|
140
|
-
}: Omit<DialogProps, 'placement'> & { placement: 'center' |
|
|
143
|
+
}: Omit<DialogProps, 'placement'> & { placement: 'center' | DialogSidePlacement }) {
|
|
141
144
|
const isControlled = controlledOpen !== undefined;
|
|
142
|
-
const isSide = placement
|
|
145
|
+
const isSide = placement !== 'center';
|
|
143
146
|
const headerController = useDialogHeaderController();
|
|
144
147
|
const scrollableResolved = scrollable ?? true;
|
|
145
148
|
const { height: viewportHeight } = useWindowDimensions();
|
|
@@ -422,7 +425,7 @@ function SideSheet({
|
|
|
422
425
|
}: {
|
|
423
426
|
open: boolean;
|
|
424
427
|
onDismiss: () => void;
|
|
425
|
-
side:
|
|
428
|
+
side: DialogSidePlacement;
|
|
426
429
|
width: number;
|
|
427
430
|
inset?: DialogInset;
|
|
428
431
|
dismissOnBackdrop: boolean;
|
|
@@ -453,8 +456,16 @@ function SideSheet({
|
|
|
453
456
|
const insetBottom = inset?.bottom ?? 0;
|
|
454
457
|
const insetLeft = inset?.left ?? 0;
|
|
455
458
|
const insetRight = inset?.right ?? 0;
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
// `start`/`end` resolve to the physical edge they land on for everything a
|
|
460
|
+
// layout engine does not mirror: the slide sign, and which `inset` applies.
|
|
461
|
+
const rtl = useIsRtl();
|
|
462
|
+
const edge = physicalDialogSide(side, rtl);
|
|
463
|
+
// The POSITION of a logical placement is a logical key, resolved by Yoga
|
|
464
|
+
// against `I18nManager` — a physical `left`/`right` here would be swapped by
|
|
465
|
+
// React Native's default `swapLeftAndRightInRTL` and land on the wrong edge.
|
|
466
|
+
const anchorKey = side === 'start' ? 'insetInlineStart' : side === 'end' ? 'insetInlineEnd' : side;
|
|
467
|
+
const anchorInset = edge === 'left' ? insetLeft : insetRight;
|
|
468
|
+
const oppositeInset = edge === 'left' ? insetRight : insetLeft;
|
|
458
469
|
|
|
459
470
|
const sideWidth = useMemo(() => {
|
|
460
471
|
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
@@ -491,7 +502,7 @@ function SideSheet({
|
|
|
491
502
|
if (dismissOnBackdrop) onDismiss();
|
|
492
503
|
}, [dismissOnBackdrop, onDismiss]);
|
|
493
504
|
|
|
494
|
-
const hiddenSign =
|
|
505
|
+
const hiddenSign = edge === 'left' ? -1 : 1;
|
|
495
506
|
const panelAnimatedStyle = useAnimatedStyle(() => {
|
|
496
507
|
const hidden = 1 - progress.value;
|
|
497
508
|
return {
|
|
@@ -513,11 +524,11 @@ function SideSheet({
|
|
|
513
524
|
({
|
|
514
525
|
top: insetTop,
|
|
515
526
|
bottom: insetBottom,
|
|
516
|
-
[
|
|
527
|
+
[anchorKey]: anchorInset,
|
|
517
528
|
width: sideWidth,
|
|
518
529
|
borderRadius: PANEL_RADIUS,
|
|
519
530
|
}),
|
|
520
|
-
[insetTop, insetBottom,
|
|
531
|
+
[insetTop, insetBottom, anchorKey, anchorInset, sideWidth],
|
|
521
532
|
);
|
|
522
533
|
|
|
523
534
|
if (!mounted) return null;
|
|
@@ -535,7 +546,15 @@ function SideSheet({
|
|
|
535
546
|
return (
|
|
536
547
|
// `OverlayRoot` takes this drawer's place in the open-order overlay stack.
|
|
537
548
|
// It mounts here, past the `mounted` guard, so the rank tracks OPENING.
|
|
538
|
-
|
|
549
|
+
// `onRequestClose`: the drawer is not an RN `Modal`, so Android back would
|
|
550
|
+
// otherwise pass straight to the screen behind it. A drawer that refuses
|
|
551
|
+
// backdrop dismissal still swallows the press, and stays open.
|
|
552
|
+
<OverlayRoot
|
|
553
|
+
style={[sideStyles.root, containerStyle]}
|
|
554
|
+
className={containerClassName}
|
|
555
|
+
onRequestClose={handleBackdropPress}
|
|
556
|
+
modal
|
|
557
|
+
>
|
|
539
558
|
<Backdrop
|
|
540
559
|
testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
|
|
541
560
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
@@ -45,6 +45,7 @@ import { Z_INDEX } from '../styles/z-index';
|
|
|
45
45
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
46
46
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
47
47
|
import { useTheme } from '../theme/use-theme';
|
|
48
|
+
import { useIsRtl } from '../hooks/use-is-rtl';
|
|
48
49
|
import { Context, useDialogContext, useDialogControl } from './context';
|
|
49
50
|
import { DialogBody } from './DialogContent';
|
|
50
51
|
import { DialogBottomSheet } from './DialogBottomSheet';
|
|
@@ -69,6 +70,8 @@ import {
|
|
|
69
70
|
PANEL_RADIUS,
|
|
70
71
|
SIDE_SHEET_MIN_GUTTER,
|
|
71
72
|
useResolvedPlacement,
|
|
73
|
+
physicalDialogSide,
|
|
74
|
+
type DialogSidePlacement,
|
|
72
75
|
} from './placement';
|
|
73
76
|
import type {
|
|
74
77
|
DialogAction,
|
|
@@ -196,7 +199,7 @@ function CenterOrSideDialog({
|
|
|
196
199
|
containerClassName,
|
|
197
200
|
label,
|
|
198
201
|
children,
|
|
199
|
-
}: Omit<DialogProps, 'placement'> & { placement: 'center' |
|
|
202
|
+
}: Omit<DialogProps, 'placement'> & { placement: 'center' | DialogSidePlacement }) {
|
|
200
203
|
// Inject the required @keyframes on mount (before the panel ever appears —
|
|
201
204
|
// this component renders once with `isOpen=false` prior to opening, so the
|
|
202
205
|
// stylesheet is present by the time the animated surface mounts).
|
|
@@ -647,7 +650,7 @@ function SheetSurface({
|
|
|
647
650
|
actions?: DialogAction[];
|
|
648
651
|
header?: DialogProps['header'];
|
|
649
652
|
scrollable?: boolean;
|
|
650
|
-
placement:
|
|
653
|
+
placement: DialogSidePlacement;
|
|
651
654
|
shown: boolean;
|
|
652
655
|
width: number;
|
|
653
656
|
inset?: DialogInset;
|
|
@@ -666,6 +669,7 @@ function SheetSurface({
|
|
|
666
669
|
const descriptionId = useId();
|
|
667
670
|
const headerController = useDialogHeaderController();
|
|
668
671
|
const { width: viewportWidth } = useWindowDimensions();
|
|
672
|
+
const rtl = useIsRtl();
|
|
669
673
|
|
|
670
674
|
// Defer the entry transition by a frame so the start state paints before the
|
|
671
675
|
// browser animates to `shown`. `entered` is false on the first committed
|
|
@@ -707,22 +711,26 @@ function SheetSurface({
|
|
|
707
711
|
const insetBottom = inset?.bottom ?? 0;
|
|
708
712
|
const insetLeft = inset?.left ?? 0;
|
|
709
713
|
const insetRight = inset?.right ?? 0;
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
// `start`/`end` land on a physical edge read from `useIsRtl()`. The drawer
|
|
715
|
+
// is portaled outside any `dir` context, so react-native-web would resolve
|
|
716
|
+
// a logical key left-to-right; the physical edge is the one that is right.
|
|
717
|
+
const edge = physicalDialogSide(placement, rtl);
|
|
718
|
+
const anchorInset = edge === 'left' ? insetLeft : insetRight;
|
|
719
|
+
const oppositeInset = edge === 'left' ? insetRight : insetLeft;
|
|
712
720
|
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
713
721
|
const cappedWidth = Math.max(0, Math.min(width, available));
|
|
714
|
-
const hiddenSign =
|
|
722
|
+
const hiddenSign = edge === 'left' ? '-100%' : '100%';
|
|
715
723
|
|
|
716
724
|
return {
|
|
717
725
|
top: insetTop,
|
|
718
726
|
bottom: insetBottom,
|
|
719
|
-
[
|
|
727
|
+
[edge]: anchorInset,
|
|
720
728
|
width: cappedWidth,
|
|
721
729
|
borderRadius: PANEL_RADIUS,
|
|
722
730
|
transform: [{ translateX: visible ? 0 : hiddenSign }],
|
|
723
731
|
opacity: visible ? 1 : 0,
|
|
724
732
|
};
|
|
725
|
-
}, [visible, placement, width, inset, viewportWidth]);
|
|
733
|
+
}, [visible, placement, rtl, width, inset, viewportWidth]);
|
|
726
734
|
|
|
727
735
|
const handleBackdropPress = useCallback(() => {
|
|
728
736
|
if (dismissOnBackdrop) onDismiss();
|
package/src/dialog/placement.ts
CHANGED
|
@@ -7,10 +7,27 @@ import { BREAKPOINTS } from '../styles/breakpoints';
|
|
|
7
7
|
* Anchor a `<Dialog>` can render against.
|
|
8
8
|
*
|
|
9
9
|
* - `'center'` — the default centered modal card (current behavior).
|
|
10
|
-
* - `'left'` / `'right'` — an anchored side-sheet / drawer.
|
|
10
|
+
* - `'left'` / `'right'` — an anchored side-sheet / drawer on that PHYSICAL edge.
|
|
11
|
+
* - `'start'` / `'end'` — the same drawer on the LOGICAL edge: `start` is the
|
|
12
|
+
* left in a left-to-right layout and the right in a right-to-left one, and it
|
|
13
|
+
* slides in from that edge. Resolved with `useIsRtl()`.
|
|
11
14
|
* - `'bottom'` — a bottom-sheet (drag handle, slide-up).
|
|
12
15
|
*/
|
|
13
|
-
export type DialogPlacement = 'center' | 'left' | 'right' | 'bottom';
|
|
16
|
+
export type DialogPlacement = 'center' | 'left' | 'right' | 'start' | 'end' | 'bottom';
|
|
17
|
+
|
|
18
|
+
/** The four side-sheet placements, physical and logical. */
|
|
19
|
+
export type DialogSidePlacement = 'left' | 'right' | 'start' | 'end';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The PHYSICAL edge a side placement lands on. `left`/`right` are themselves;
|
|
23
|
+
* `start` is `left` left-to-right and `right` right-to-left, `end` the reverse.
|
|
24
|
+
* The panel's `translateX` sign, which no layout engine mirrors, follows this.
|
|
25
|
+
*/
|
|
26
|
+
export function physicalDialogSide(side: DialogSidePlacement, rtl: boolean): 'left' | 'right' {
|
|
27
|
+
if (side === 'start') return rtl ? 'right' : 'left';
|
|
28
|
+
if (side === 'end') return rtl ? 'left' : 'right';
|
|
29
|
+
return side;
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* A `<Dialog>` placement that may vary by viewport width.
|
package/src/dialog/types.ts
CHANGED
|
@@ -256,8 +256,9 @@ export type DialogProps = React.PropsWithChildren<{
|
|
|
256
256
|
actions?: DialogAction[];
|
|
257
257
|
/**
|
|
258
258
|
* Surface anchor. Defaults to `'center'` (the centered modal — current
|
|
259
|
-
* behavior). `'left'`/`'right'` render an anchored side-sheet
|
|
260
|
-
*
|
|
259
|
+
* behavior). `'left'`/`'right'` render an anchored side-sheet on that
|
|
260
|
+
* physical edge; `'start'`/`'end'` on the logical edge (mirrored, with its
|
|
261
|
+
* slide-in, in a right-to-left layout); `'bottom'` a bottom-sheet. A responsive map resolves by viewport width, e.g.
|
|
261
262
|
* `{ base: 'bottom', md: 'left' }` (drawer on wide, sheet on narrow).
|
|
262
263
|
*/
|
|
263
264
|
placement?: ResponsiveDialogPlacement;
|
package/src/loading/Loading.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { resolveBloomColors } from '../appearance/colors';
|
|
|
18
18
|
import { useBloomAppearance } from '../appearance';
|
|
19
19
|
import { animation } from '../styles/tokens';
|
|
20
20
|
import { useTheme } from '../theme/use-theme';
|
|
21
|
-
import { bindLoading, SIZE_CONFIG, styles } from './LoadingBase';
|
|
21
|
+
import { bindLoading, LoadingRoot, SIZE_CONFIG, styles } from './LoadingBase';
|
|
22
22
|
import { SpinnerIcon } from './SpinnerIcon';
|
|
23
23
|
import type { TopLoadingProps } from './types';
|
|
24
24
|
|
|
@@ -31,6 +31,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
31
31
|
iconSize,
|
|
32
32
|
heightOffset = 0,
|
|
33
33
|
spinnerIcon,
|
|
34
|
+
accessibilityLabel,
|
|
34
35
|
testID,
|
|
35
36
|
}) => {
|
|
36
37
|
const theme = useTheme();
|
|
@@ -69,7 +70,10 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
69
70
|
return (
|
|
70
71
|
<Animated.View style={[styles.topContainer, containerAnimated]} testID={testID}>
|
|
71
72
|
<Animated.View style={[styles.topLoadingView, { height: targetHeight }, innerAnimated, style]}>
|
|
72
|
-
{
|
|
73
|
+
{/* Named only while it is showing: a collapsed bar is not a wait. */}
|
|
74
|
+
<LoadingRoot accessibilityLabel={showLoading ? accessibilityLabel : undefined}>
|
|
75
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
|
|
76
|
+
</LoadingRoot>
|
|
73
77
|
</Animated.View>
|
|
74
78
|
</Animated.View>
|
|
75
79
|
);
|
|
@@ -16,7 +16,7 @@ import { useBloomAppearance } from '../appearance';
|
|
|
16
16
|
import { animation } from '../styles/tokens';
|
|
17
17
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
18
18
|
import { useTheme } from '../theme/use-theme';
|
|
19
|
-
import { bindLoading, SIZE_CONFIG, styles } from './LoadingBase';
|
|
19
|
+
import { bindLoading, LoadingRoot, SIZE_CONFIG, styles } from './LoadingBase';
|
|
20
20
|
import { SpinnerIcon } from './SpinnerIcon.web';
|
|
21
21
|
import type { TopLoadingProps } from './types';
|
|
22
22
|
|
|
@@ -41,6 +41,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
41
41
|
iconSize,
|
|
42
42
|
heightOffset = 0,
|
|
43
43
|
spinnerIcon,
|
|
44
|
+
accessibilityLabel,
|
|
44
45
|
testID,
|
|
45
46
|
}) => {
|
|
46
47
|
const theme = useTheme();
|
|
@@ -84,7 +85,10 @@ const TopLoading: React.FC<TopLoadingProps> = ({
|
|
|
84
85
|
style,
|
|
85
86
|
]}
|
|
86
87
|
>
|
|
87
|
-
{
|
|
88
|
+
{/* Named only while it is showing: a collapsed bar is not a wait. */}
|
|
89
|
+
<LoadingRoot accessibilityLabel={showLoading ? accessibilityLabel : undefined}>
|
|
90
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
|
|
91
|
+
</LoadingRoot>
|
|
88
92
|
</View>
|
|
89
93
|
</View>
|
|
90
94
|
);
|
|
@@ -23,7 +23,7 @@ import { normalizeBloomSize } from '../appearance/legacy';
|
|
|
23
23
|
import { resolveBloomColors } from '../appearance/colors';
|
|
24
24
|
import { useBloomAppearance } from '../appearance';
|
|
25
25
|
import React, { memo } from 'react';
|
|
26
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
26
|
+
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
27
27
|
|
|
28
28
|
import { useTheme } from '../theme/use-theme';
|
|
29
29
|
import type {
|
|
@@ -41,6 +41,54 @@ export const SIZE_CONFIG = {
|
|
|
41
41
|
lg: { spinner: 44, text: 16 },
|
|
42
42
|
} as const;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The indicator's root: an indeterminate, NAMED `progressbar` when the caller
|
|
46
|
+
* gave `accessibilityLabel`, a plain view (today's behaviour) when not.
|
|
47
|
+
*
|
|
48
|
+
* Flat `aria-*` because react-native-web drops `accessibilityState` /
|
|
49
|
+
* `accessibilityValue`; React Native folds them back. `aria-valuenow` is
|
|
50
|
+
* deliberately absent (`undefined`): that is what makes a progressbar
|
|
51
|
+
* INDETERMINATE in ARIA, and a spinner has no amount done to report. The role
|
|
52
|
+
* is a literal on its own branch so `aria-state-source-census.test.ts` can read
|
|
53
|
+
* it; a computed role would be invisible to it.
|
|
54
|
+
*/
|
|
55
|
+
export function LoadingRoot({
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
style,
|
|
58
|
+
testID,
|
|
59
|
+
children,
|
|
60
|
+
}: {
|
|
61
|
+
accessibilityLabel?: string;
|
|
62
|
+
style?: StyleProp<ViewStyle>;
|
|
63
|
+
testID?: string;
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
}) {
|
|
66
|
+
if (accessibilityLabel) {
|
|
67
|
+
return (
|
|
68
|
+
<View
|
|
69
|
+
role="progressbar"
|
|
70
|
+
accessible
|
|
71
|
+
accessibilityLabel={accessibilityLabel}
|
|
72
|
+
aria-valuemin={0}
|
|
73
|
+
aria-valuemax={100}
|
|
74
|
+
aria-valuenow={undefined}
|
|
75
|
+
aria-busy
|
|
76
|
+
style={style}
|
|
77
|
+
testID={testID}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Nothing to carry (the `top` variant's inner slot): no extra node at all.
|
|
84
|
+
if (style === undefined && testID === undefined) return <>{children}</>;
|
|
85
|
+
return (
|
|
86
|
+
<View style={style} testID={testID}>
|
|
87
|
+
{children}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
44
92
|
/** The two platform pieces a fork supplies. */
|
|
45
93
|
export interface LoadingPlatform {
|
|
46
94
|
/** Forked: react-native-svg on native, a DOM `<svg>` on web. */
|
|
@@ -61,6 +109,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
61
109
|
showText = true,
|
|
62
110
|
iconSize,
|
|
63
111
|
spinnerIcon,
|
|
112
|
+
accessibilityLabel,
|
|
64
113
|
testID,
|
|
65
114
|
}) => {
|
|
66
115
|
const theme = useTheme();
|
|
@@ -71,7 +120,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
71
120
|
const textColor = color ?? theme.colors.textSecondary;
|
|
72
121
|
|
|
73
122
|
return (
|
|
74
|
-
<
|
|
123
|
+
<LoadingRoot accessibilityLabel={accessibilityLabel} style={[styles.container, style]} testID={testID}>
|
|
75
124
|
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} className={className} />}
|
|
76
125
|
{showText && text && (
|
|
77
126
|
<Text
|
|
@@ -84,7 +133,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
84
133
|
{text}
|
|
85
134
|
</Text>
|
|
86
135
|
)}
|
|
87
|
-
</
|
|
136
|
+
</LoadingRoot>
|
|
88
137
|
);
|
|
89
138
|
};
|
|
90
139
|
|
|
@@ -96,6 +145,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
96
145
|
style,
|
|
97
146
|
textStyle,
|
|
98
147
|
spinnerIcon,
|
|
148
|
+
accessibilityLabel,
|
|
99
149
|
testID,
|
|
100
150
|
}) => {
|
|
101
151
|
const theme = useTheme();
|
|
@@ -105,7 +155,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
105
155
|
const textColor = theme.colors.textSecondary;
|
|
106
156
|
|
|
107
157
|
return (
|
|
108
|
-
<
|
|
158
|
+
<LoadingRoot accessibilityLabel={accessibilityLabel} style={[styles.inlineContainer, style]} testID={testID}>
|
|
109
159
|
{spinnerIcon ?? <SpinnerIcon size={sizeConfig.spinner} color={spinnerColor} />}
|
|
110
160
|
{text && (
|
|
111
161
|
<Text
|
|
@@ -117,7 +167,7 @@ export function bindLoading({ SpinnerIcon, TopLoading }: LoadingPlatform) {
|
|
|
117
167
|
{text}
|
|
118
168
|
</Text>
|
|
119
169
|
)}
|
|
120
|
-
</
|
|
170
|
+
</LoadingRoot>
|
|
121
171
|
);
|
|
122
172
|
};
|
|
123
173
|
|
package/src/loading/types.ts
CHANGED
|
@@ -20,6 +20,17 @@ interface BaseLoadingProps {
|
|
|
20
20
|
style?: ViewStyle;
|
|
21
21
|
/** Whether loading is active (for animated variants) */
|
|
22
22
|
showLoading?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Names the indicator and makes it an indeterminate `progressbar` to
|
|
25
|
+
* assistive technology (`role="progressbar"` with `aria-label` and
|
|
26
|
+
* `aria-busy` on web; the same role, label and busy state natively). A
|
|
27
|
+
* spinner draws no words, so nothing else can say what is loading.
|
|
28
|
+
*
|
|
29
|
+
* Opt-in: without it the indicator carries no role, as before — which is
|
|
30
|
+
* right when it sits INSIDE a named progressbar already (an upload overlay),
|
|
31
|
+
* where a second one would announce the same wait twice.
|
|
32
|
+
*/
|
|
33
|
+
accessibilityLabel?: string;
|
|
23
34
|
testID?: string;
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -81,7 +81,7 @@ const PULSE_SCALE = 0.18;
|
|
|
81
81
|
|
|
82
82
|
/** One full-size layer that centres whatever it holds on the box's centre. */
|
|
83
83
|
const layerStyle: ViewStyle = {
|
|
84
|
-
...StyleSheet.
|
|
84
|
+
...StyleSheet.absoluteFill,
|
|
85
85
|
alignItems: 'center',
|
|
86
86
|
justifyContent: 'center',
|
|
87
87
|
};
|
package/src/mail-list/parts.tsx
CHANGED
|
@@ -77,7 +77,7 @@ export function MailRowLink({
|
|
|
77
77
|
};
|
|
78
78
|
const interactive = Boolean(onPress || href || onLongPress);
|
|
79
79
|
const style: WebCssStyle = {
|
|
80
|
-
...StyleSheet.
|
|
80
|
+
...StyleSheet.absoluteFill,
|
|
81
81
|
borderRadius: MAIL_ROW_RADIUS,
|
|
82
82
|
'--bloom-mail-ring': paint.accent,
|
|
83
83
|
};
|
package/src/media-card/parts.tsx
CHANGED
|
@@ -287,7 +287,7 @@ export function CardLink({ name, onPress, href, onLongPress, selected = false, r
|
|
|
287
287
|
if (!IS_WEB && href) void Linking.openURL(href).catch(() => undefined);
|
|
288
288
|
};
|
|
289
289
|
const style: WebCssStyle = {
|
|
290
|
-
...StyleSheet.
|
|
290
|
+
...StyleSheet.absoluteFill,
|
|
291
291
|
borderRadius: radius,
|
|
292
292
|
'--bloom-media-card-ring': paint.ring,
|
|
293
293
|
};
|
package/src/overlay/Overlay.tsx
CHANGED
|
@@ -57,6 +57,7 @@ import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
|
57
57
|
import { StyledView } from '../styles/styled-primitives';
|
|
58
58
|
import { SurfaceLevelProvider } from '../styles/surface-levels';
|
|
59
59
|
import { layerForRank, type OverlayLayer } from './stack';
|
|
60
|
+
import { useHardwareBack } from './use-hardware-back';
|
|
60
61
|
import { useOverlayLayer } from './use-overlay-layer';
|
|
61
62
|
import type { OverlayRootProps, BackdropProps } from './types';
|
|
62
63
|
|
|
@@ -109,32 +110,43 @@ export const BACKDROP_DIM_OPACITY = 0.28;
|
|
|
109
110
|
* every Bloom surface already puts it. Descendants that need to order
|
|
110
111
|
* themselves within the surface read `useOverlayLayerContext()`.
|
|
111
112
|
*/
|
|
112
|
-
export function OverlayRoot({
|
|
113
|
+
export function OverlayRoot({
|
|
114
|
+
children,
|
|
115
|
+
className,
|
|
116
|
+
style,
|
|
117
|
+
testID,
|
|
118
|
+
zIndex,
|
|
119
|
+
onRequestClose,
|
|
120
|
+
modal = false,
|
|
121
|
+
}: OverlayRootProps) {
|
|
122
|
+
// On the root rather than in each surface: every portaled surface mounts one
|
|
123
|
+
// inside its open guard, so this is the one place that cannot be forgotten.
|
|
124
|
+
// The gallery once forgot it and Android back finished the whole activity.
|
|
125
|
+
useHardwareBack(onRequestClose);
|
|
113
126
|
// Split into two components rather than branching on the hook: a pinned root
|
|
114
127
|
// must not CONSUME a rank either. The toast host is pinned and mounts for the
|
|
115
128
|
// whole life of the app, so holding a rank would keep the live set permanently
|
|
116
129
|
// non-empty — the counter would never reset and depths would climb for the
|
|
117
130
|
// rest of the session.
|
|
118
131
|
return zIndex === undefined ? (
|
|
119
|
-
<StackedOverlayRoot className={className} style={style} testID={testID}>
|
|
132
|
+
<StackedOverlayRoot className={className} style={style} testID={testID} modal={modal}>
|
|
120
133
|
{children}
|
|
121
134
|
</StackedOverlayRoot>
|
|
122
135
|
) : (
|
|
123
|
-
<PinnedOverlayRoot zIndex={zIndex} className={className} style={style} testID={testID}>
|
|
136
|
+
<PinnedOverlayRoot zIndex={zIndex} className={className} style={style} testID={testID} modal={modal}>
|
|
124
137
|
{children}
|
|
125
138
|
</PinnedOverlayRoot>
|
|
126
139
|
);
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}: Omit<OverlayRootProps, 'zIndex'>) {
|
|
142
|
+
type OverlayRootViewProps = Pick<OverlayRootProps, 'children' | 'className' | 'style' | 'testID'> & {
|
|
143
|
+
modal: boolean;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function StackedOverlayRoot({ children, className, style, testID, modal }: OverlayRootViewProps) {
|
|
135
147
|
const layer = useOverlayLayer();
|
|
136
148
|
return (
|
|
137
|
-
<OverlayRootView layer={layer} className={className} style={style} testID={testID}>
|
|
149
|
+
<OverlayRootView layer={layer} className={className} style={style} testID={testID} modal={modal}>
|
|
138
150
|
{children}
|
|
139
151
|
</OverlayRootView>
|
|
140
152
|
);
|
|
@@ -146,7 +158,8 @@ function PinnedOverlayRoot({
|
|
|
146
158
|
style,
|
|
147
159
|
testID,
|
|
148
160
|
zIndex,
|
|
149
|
-
|
|
161
|
+
modal,
|
|
162
|
+
}: OverlayRootViewProps & { zIndex: number }) {
|
|
150
163
|
// Outside the stack, so descendants must not read stack depths from it
|
|
151
164
|
// either — every slot is the pinned depth.
|
|
152
165
|
const layer = useMemo(
|
|
@@ -154,7 +167,7 @@ function PinnedOverlayRoot({
|
|
|
154
167
|
[zIndex],
|
|
155
168
|
);
|
|
156
169
|
return (
|
|
157
|
-
<OverlayRootView layer={layer} className={className} style={style} testID={testID}>
|
|
170
|
+
<OverlayRootView layer={layer} className={className} style={style} testID={testID} modal={modal}>
|
|
158
171
|
{children}
|
|
159
172
|
</OverlayRootView>
|
|
160
173
|
);
|
|
@@ -166,14 +179,19 @@ function OverlayRootView({
|
|
|
166
179
|
style,
|
|
167
180
|
testID,
|
|
168
181
|
layer,
|
|
169
|
-
|
|
182
|
+
modal,
|
|
183
|
+
}: OverlayRootViewProps & { layer: OverlayLayer }) {
|
|
170
184
|
return (
|
|
171
185
|
<OverlayLayerContext.Provider value={layer}>
|
|
172
186
|
{/* `pointerEvents` stays a PROP: react-native-web resolves the RN-only
|
|
173
187
|
`box-none` from the prop path only, and as a style entry it is silently
|
|
174
|
-
dropped — which makes the whole portaled surface click-through.
|
|
188
|
+
dropped — which makes the whole portaled surface click-through.
|
|
189
|
+
`accessibilityViewIsModal` sits HERE, not on the panel: iOS hides only
|
|
190
|
+
the modal view's SIBLINGS, and at the native outlet this root is the
|
|
191
|
+
sibling of the app content, where the panel is not. */}
|
|
175
192
|
<StyledView
|
|
176
193
|
pointerEvents="box-none"
|
|
194
|
+
accessibilityViewIsModal={modal && Platform.OS !== 'web' ? true : undefined}
|
|
177
195
|
className={className}
|
|
178
196
|
style={[styles.root, { zIndex: layer.root }, style]}
|
|
179
197
|
testID={testID}
|
package/src/overlay/types.ts
CHANGED
|
@@ -23,6 +23,24 @@ export interface OverlayRootProps {
|
|
|
23
23
|
* `./stack.ts` exists to remove.
|
|
24
24
|
*/
|
|
25
25
|
zIndex?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Android hardware back while this surface is open. A portaled surface is not
|
|
28
|
+
* an RN `Modal`, so nothing else consumes the press: without this the app's
|
|
29
|
+
* own back handling runs and, at the root of a stack, finishes the activity —
|
|
30
|
+
* the surface "closes" by closing the whole app. Called and consumed while the
|
|
31
|
+
* root is mounted; later-opened surfaces register later, and `BackHandler`
|
|
32
|
+
* runs the newest listener first, so the top surface closes first. A blocking
|
|
33
|
+
* surface still passes a handler (a no-op) so the press is swallowed rather
|
|
34
|
+
* than falling through to the screen underneath.
|
|
35
|
+
*/
|
|
36
|
+
onRequestClose?: () => void;
|
|
37
|
+
/**
|
|
38
|
+
* The surface is modal: a screen reader must not wander into the app behind
|
|
39
|
+
* it. Native only — sets `accessibilityViewIsModal` on this root, which is the
|
|
40
|
+
* app content's sibling at the portal outlet (iOS confines VoiceOver to
|
|
41
|
+
* it). Web surfaces already carry `aria-modal` on their own dialog node.
|
|
42
|
+
*/
|
|
43
|
+
modal?: boolean;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export interface BackdropProps {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { BackHandler, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Consume Android's hardware back for as long as the caller is mounted with a
|
|
6
|
+
* handler, calling the CURRENT `onBack` (read through a ref, so a new closure
|
|
7
|
+
* each render does not re-register and lose its place in the LIFO order).
|
|
8
|
+
*
|
|
9
|
+
* `BackHandler` runs the newest listener first, so surfaces opened later close
|
|
10
|
+
* first. No-op off Android and when `BackHandler` is absent (web bundles, and
|
|
11
|
+
* some test environments' react-native mocks).
|
|
12
|
+
*/
|
|
13
|
+
export function useHardwareBack(onBack: (() => void) | undefined): void {
|
|
14
|
+
const onBackRef = useRef(onBack);
|
|
15
|
+
onBackRef.current = onBack;
|
|
16
|
+
const active = onBack !== undefined;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!active || Platform.OS !== 'android') return;
|
|
20
|
+
if (typeof BackHandler?.addEventListener !== 'function') return;
|
|
21
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
22
|
+
onBackRef.current?.();
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
return () => subscription.remove();
|
|
26
|
+
}, [active]);
|
|
27
|
+
}
|
|
@@ -82,6 +82,7 @@ function PriceSummaryComponent({
|
|
|
82
82
|
label={line.label}
|
|
83
83
|
sublabel={line.sublabel}
|
|
84
84
|
amount={line.amount}
|
|
85
|
+
secondaryAmount={line.secondaryAmount}
|
|
85
86
|
tone={line.tone}
|
|
86
87
|
state={line.state}
|
|
87
88
|
info={line.info}
|
|
@@ -137,6 +138,15 @@ function PriceSummaryComponent({
|
|
|
137
138
|
>
|
|
138
139
|
{total.amount ?? pendingPlaceholder}
|
|
139
140
|
</Text>
|
|
141
|
+
{total.secondaryAmount ? (
|
|
142
|
+
<Text
|
|
143
|
+
variant="body-2-regular"
|
|
144
|
+
testID={testID ? `${testID}-total-secondary-amount` : undefined}
|
|
145
|
+
style={{ color: paint.textSecondary, fontVariant: ['tabular-nums'], textAlign: 'right' }}
|
|
146
|
+
>
|
|
147
|
+
{total.secondaryAmount}
|
|
148
|
+
</Text>
|
|
149
|
+
) : null}
|
|
140
150
|
{totalCaveat ? (
|
|
141
151
|
<Text
|
|
142
152
|
variant="caption-1-regular"
|
|
@@ -27,6 +27,8 @@ import type { PriceSummaryLineProps } from './types';
|
|
|
27
27
|
* state "Estimated" / "Pending" in caption-1 under the amount, so the
|
|
28
28
|
* caveat travels with the NUMBER rather than with the label; the
|
|
29
29
|
* row keeps its own reading order for a screen reader
|
|
30
|
+
* secondary `secondaryAmount` (a second currency) in body-2-regular
|
|
31
|
+
* secondary under the amount, tabular, right-aligned
|
|
30
32
|
* info a 32 round glyph after the label, opening a `Popover`
|
|
31
33
|
* height 32 minimum, whether or not the line has the info affordance
|
|
32
34
|
*
|
|
@@ -38,6 +40,7 @@ function PriceSummaryLineComponent({
|
|
|
38
40
|
label,
|
|
39
41
|
sublabel,
|
|
40
42
|
amount,
|
|
43
|
+
secondaryAmount,
|
|
41
44
|
tone = 'default',
|
|
42
45
|
state = 'final',
|
|
43
46
|
info,
|
|
@@ -120,6 +123,15 @@ function PriceSummaryLineComponent({
|
|
|
120
123
|
>
|
|
121
124
|
{amount ?? pendingPlaceholder}
|
|
122
125
|
</Text>
|
|
126
|
+
{secondaryAmount ? (
|
|
127
|
+
<Text
|
|
128
|
+
variant="body-2-regular"
|
|
129
|
+
testID={testID ? `${testID}-secondary-amount` : undefined}
|
|
130
|
+
style={{ color: paint.textSecondary, fontVariant: ['tabular-nums'], textAlign: 'right' }}
|
|
131
|
+
>
|
|
132
|
+
{secondaryAmount}
|
|
133
|
+
</Text>
|
|
134
|
+
) : null}
|
|
123
135
|
{caveat ? (
|
|
124
136
|
<Text
|
|
125
137
|
variant="caption-1-regular"
|