@oxyhq/bloom 0.72.1 → 0.73.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/lib/commonjs/avatar-group/AvatarGroup.web.js +22 -21
- package/lib/commonjs/avatar-group/AvatarGroup.web.js.map +1 -1
- package/lib/commonjs/bottom-sheet/index.web.js +11 -6
- package/lib/commonjs/bottom-sheet/index.web.js.map +1 -1
- package/lib/commonjs/context-menu/index.web.js +20 -22
- package/lib/commonjs/context-menu/index.web.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +66 -70
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +49 -51
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/fonts/font-data.web.js +22 -0
- package/lib/commonjs/fonts/font-data.web.js.map +1 -0
- package/lib/commonjs/menu/index.web.js +15 -11
- package/lib/commonjs/menu/index.web.js.map +1 -1
- package/lib/commonjs/overlay/index.js +186 -39
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/overlay/stack.js +170 -0
- package/lib/commonjs/overlay/stack.js.map +1 -0
- package/lib/commonjs/overlay/use-overlay-layer.js +37 -0
- package/lib/commonjs/overlay/use-overlay-layer.js.map +1 -0
- package/lib/commonjs/popover/index.web.js +31 -34
- package/lib/commonjs/popover/index.web.js.map +1 -1
- package/lib/commonjs/prompt-input/PromptInput.js +33 -29
- package/lib/commonjs/prompt-input/PromptInput.js.map +1 -1
- package/lib/commonjs/select/index.web.js +20 -22
- package/lib/commonjs/select/index.web.js.map +1 -1
- package/lib/commonjs/styles/z-index.js +33 -34
- package/lib/commonjs/styles/z-index.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +3 -6
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
- package/lib/commonjs/toast/ToastHost.js +9 -5
- package/lib/commonjs/toast/ToastHost.js.map +1 -1
- package/lib/commonjs/tooltip/index.js +9 -6
- package/lib/commonjs/tooltip/index.js.map +1 -1
- package/lib/commonjs/tooltip/index.web.js +8 -2
- package/lib/commonjs/tooltip/index.web.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/avatar-group/AvatarGroup.web.js +21 -20
- package/lib/module/avatar-group/AvatarGroup.web.js.map +1 -1
- package/lib/module/bottom-sheet/index.web.js +11 -6
- package/lib/module/bottom-sheet/index.web.js.map +1 -1
- package/lib/module/context-menu/index.web.js +21 -23
- package/lib/module/context-menu/index.web.js.map +1 -1
- package/lib/module/dialog/Dialog.js +67 -71
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +50 -52
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/fonts/font-data.web.js +18 -0
- package/lib/module/fonts/font-data.web.js.map +1 -0
- package/lib/module/menu/index.web.js +16 -12
- package/lib/module/menu/index.web.js.map +1 -1
- package/lib/module/overlay/index.js +152 -40
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/overlay/stack.js +161 -0
- package/lib/module/overlay/stack.js.map +1 -0
- package/lib/module/overlay/use-overlay-layer.js +33 -0
- package/lib/module/overlay/use-overlay-layer.js.map +1 -0
- package/lib/module/popover/index.web.js +32 -34
- package/lib/module/popover/index.web.js.map +1 -1
- package/lib/module/prompt-input/PromptInput.js +33 -29
- package/lib/module/prompt-input/PromptInput.js.map +1 -1
- package/lib/module/select/index.web.js +21 -23
- package/lib/module/select/index.web.js.map +1 -1
- package/lib/module/styles/z-index.js +32 -30
- package/lib/module/styles/z-index.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +3 -6
- package/lib/module/surfaces/SurfaceHost.js.map +1 -1
- package/lib/module/toast/ToastHost.js +10 -6
- package/lib/module/toast/ToastHost.js.map +1 -1
- package/lib/module/tooltip/index.js +9 -6
- package/lib/module/tooltip/index.js.map +1 -1
- package/lib/module/tooltip/index.web.js +8 -2
- package/lib/module/tooltip/index.web.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/context-menu/index.web.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/types.d.ts +0 -10
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/font-data.web.d.ts +5 -0
- package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts +47 -10
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/stack.d.ts +122 -0
- package/lib/typescript/commonjs/overlay/stack.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts +7 -0
- package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts.map +1 -0
- package/lib/typescript/commonjs/popover/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/prompt-input/PromptInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/z-index.d.ts +42 -29
- package/lib/typescript/commonjs/styles/z-index.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/module/context-menu/index.web.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/types.d.ts +0 -10
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/fonts/font-data.web.d.ts +5 -0
- package/lib/typescript/module/fonts/font-data.web.d.ts.map +1 -0
- package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts +47 -10
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/overlay/stack.d.ts +122 -0
- package/lib/typescript/module/overlay/stack.d.ts.map +1 -0
- package/lib/typescript/module/overlay/use-overlay-layer.d.ts +7 -0
- package/lib/typescript/module/overlay/use-overlay-layer.d.ts.map +1 -0
- package/lib/typescript/module/popover/index.web.d.ts.map +1 -1
- package/lib/typescript/module/prompt-input/PromptInput.d.ts.map +1 -1
- package/lib/typescript/module/select/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/z-index.d.ts +42 -29
- package/lib/typescript/module/styles/z-index.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/module/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/module/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/ToastHostWebFork.test.ts +11 -3
- package/src/__tests__/backdrop-nesting.test.tsx +95 -0
- package/src/__tests__/overlay-stack-order.test.tsx +270 -0
- package/src/avatar-group/AvatarGroup.web.tsx +35 -31
- package/src/bottom-sheet/index.web.tsx +10 -5
- package/src/context-menu/index.web.tsx +33 -30
- package/src/dialog/Dialog.tsx +9 -18
- package/src/dialog/Dialog.web.tsx +62 -60
- package/src/dialog/types.ts +0 -10
- package/src/fonts/font-data.web.ts +15 -0
- package/src/menu/index.web.tsx +33 -22
- package/src/overlay/OverlayStacking.stories.tsx +207 -0
- package/src/overlay/index.tsx +161 -40
- package/src/overlay/stack.ts +166 -0
- package/src/overlay/use-overlay-layer.ts +39 -0
- package/src/popover/index.web.tsx +47 -44
- package/src/prompt-input/PromptInput.tsx +36 -29
- package/src/select/index.web.tsx +35 -32
- package/src/styles/z-index.ts +32 -34
- package/src/surfaces/SurfaceHost.tsx +3 -10
- package/src/toast/ToastHost.tsx +9 -6
- package/src/tooltip/index.tsx +14 -7
- package/src/tooltip/index.web.tsx +8 -2
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +4 -4
package/src/menu/index.web.tsx
CHANGED
|
@@ -15,8 +15,9 @@ import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'rea
|
|
|
15
15
|
import { useTheme } from '../theme/use-theme';
|
|
16
16
|
import { Text } from '../typography';
|
|
17
17
|
import type { DialogControlProps } from '../dialog/types';
|
|
18
|
+
import { OverlayRoot } from '../overlay';
|
|
18
19
|
import { Portal } from '../portal/index.web';
|
|
19
|
-
import {
|
|
20
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
20
21
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
21
22
|
import { resolveDropdownPlacement } from '../overlay/dropdown-placement';
|
|
22
23
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
@@ -37,7 +38,6 @@ import type {
|
|
|
37
38
|
|
|
38
39
|
export { useMenuContext };
|
|
39
40
|
|
|
40
|
-
const menuZIndex = createDropdownZIndex();
|
|
41
41
|
const VIEWPORT_GUTTER = 8;
|
|
42
42
|
const MENU_OFFSET = 6;
|
|
43
43
|
|
|
@@ -266,24 +266,33 @@ export function MenuContent({
|
|
|
266
266
|
|
|
267
267
|
return (
|
|
268
268
|
<Portal>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
269
|
+
{/* `OverlayRoot` takes this menu's place in the open-order overlay stack.
|
|
270
|
+
The dropdown used to carry a fixed `zIndex` on the `dropdown` rung
|
|
271
|
+
(41), below the `overlay` rung a Dialog sits on (50/60) — so a menu
|
|
272
|
+
opened from inside a dialog rendered behind that dialog, whichever
|
|
273
|
+
opened last. It is `box-none`, so the area around the dropdown stays
|
|
274
|
+
click-through and the existing outside-press dismissal still sees the
|
|
275
|
+
press. */}
|
|
276
|
+
<OverlayRoot>
|
|
277
|
+
<View
|
|
278
|
+
ref={attachDropdown}
|
|
279
|
+
style={[
|
|
280
|
+
styles.dropdown,
|
|
281
|
+
{
|
|
282
|
+
backgroundColor: theme.isDark
|
|
283
|
+
? theme.colors.backgroundSecondary
|
|
284
|
+
: theme.colors.background,
|
|
285
|
+
borderColor: theme.colors.borderLight,
|
|
286
|
+
...bloomShadowStyle('m'),
|
|
287
|
+
},
|
|
288
|
+
style,
|
|
289
|
+
styles.portaledDropdown,
|
|
290
|
+
position,
|
|
291
|
+
]}
|
|
292
|
+
>
|
|
293
|
+
{children}
|
|
294
|
+
</View>
|
|
295
|
+
</OverlayRoot>
|
|
287
296
|
</Portal>
|
|
288
297
|
);
|
|
289
298
|
}
|
|
@@ -405,7 +414,10 @@ const styles = StyleSheet.create({
|
|
|
405
414
|
position: 'relative',
|
|
406
415
|
},
|
|
407
416
|
openRoot: {
|
|
408
|
-
|
|
417
|
+
// The INLINE trigger wrapper, still in the app's own document flow — this
|
|
418
|
+
// only lifts it over adjacent in-flow content while the menu is open. The
|
|
419
|
+
// portaled dropdown's depth is the overlay stack's business, not this.
|
|
420
|
+
zIndex: Z_INDEX.dropdown,
|
|
409
421
|
},
|
|
410
422
|
dropdown: {
|
|
411
423
|
// Fixed from the outset, not only once positioned: the `Portal` root is a
|
|
@@ -420,7 +432,6 @@ const styles = StyleSheet.create({
|
|
|
420
432
|
padding: 4,
|
|
421
433
|
borderWidth: 1,
|
|
422
434
|
overflow: 'hidden',
|
|
423
|
-
zIndex: menuZIndex.surface,
|
|
424
435
|
// Overlay elevation applied at the usage site via `bloomShadowStyle('m')`.
|
|
425
436
|
minWidth: 180,
|
|
426
437
|
},
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stacked-overlay stories.
|
|
3
|
+
*
|
|
4
|
+
* These exist because overlay stacking used to be decided by hand-picked
|
|
5
|
+
* `zIndex` constants, so the answer to "which surface is on top" depended on
|
|
6
|
+
* WHICH KIND of surface each one was rather than on which one the user opened
|
|
7
|
+
* last. The failure was silent and total: the later surface rendered perfectly,
|
|
8
|
+
* fully interactive, entirely behind the earlier one — unreachable and
|
|
9
|
+
* undismissable.
|
|
10
|
+
*
|
|
11
|
+
* Every story opens a second surface from INSIDE a first one and puts a button
|
|
12
|
+
* in the second surface that writes into a result line. That makes the check a
|
|
13
|
+
* real one: drive it with actual input at the button's coordinates and the
|
|
14
|
+
* result only changes if the second surface genuinely received the press. A
|
|
15
|
+
* geometry read (`zIndex`, bounding boxes) would pass on a surface the user
|
|
16
|
+
* cannot touch. `src/__tests__/overlay-stack-order.test.ts` covers the ordering
|
|
17
|
+
* rule itself; `scripts/verify-overlay-stacking.mjs` drives these stories in a
|
|
18
|
+
* real browser.
|
|
19
|
+
*/
|
|
20
|
+
import React, { useRef, useState } from 'react';
|
|
21
|
+
import { Text, View } from 'react-native';
|
|
22
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
23
|
+
|
|
24
|
+
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
25
|
+
import { Button } from '../button';
|
|
26
|
+
import { Dialog } from '../dialog/Dialog';
|
|
27
|
+
import { useDialogControl } from '../dialog/context';
|
|
28
|
+
import { Menu, MenuContent, MenuItem, MenuTrigger } from '../menu';
|
|
29
|
+
|
|
30
|
+
const meta: Meta = {
|
|
31
|
+
title: 'Components/Overlay stacking',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
|
|
36
|
+
type Story = StoryObj;
|
|
37
|
+
|
|
38
|
+
/** The line the harness reads. Starts at `idle`; the top surface writes to it. */
|
|
39
|
+
function Result({ value }: { value: string }) {
|
|
40
|
+
return <Text testID="result">result: {value}</Text>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The reported bug: a confirm dialog opened from a control inside an open bottom
|
|
45
|
+
* sheet. The dialog opens LAST, so it must be the surface that takes the press.
|
|
46
|
+
*/
|
|
47
|
+
function DialogOverSheet() {
|
|
48
|
+
const sheetRef = useRef<BottomSheetRef>(null);
|
|
49
|
+
const dialogControl = useDialogControl();
|
|
50
|
+
const [result, setResult] = useState('idle');
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<Button testID="open-first" onPress={() => sheetRef.current?.present()}>
|
|
55
|
+
Open sheet
|
|
56
|
+
</Button>
|
|
57
|
+
<Result value={result} />
|
|
58
|
+
|
|
59
|
+
<BottomSheet ref={sheetRef}>
|
|
60
|
+
<View style={{ padding: 24, gap: 12 }}>
|
|
61
|
+
<Text style={{ fontSize: 20, fontWeight: '700' }}>Post options</Text>
|
|
62
|
+
<Button
|
|
63
|
+
testID="open-second"
|
|
64
|
+
variant="secondary"
|
|
65
|
+
onPress={() => dialogControl.open()}
|
|
66
|
+
>
|
|
67
|
+
Delete post
|
|
68
|
+
</Button>
|
|
69
|
+
</View>
|
|
70
|
+
</BottomSheet>
|
|
71
|
+
|
|
72
|
+
<Dialog
|
|
73
|
+
control={dialogControl}
|
|
74
|
+
testID="confirm-dialog"
|
|
75
|
+
title="Delete post?"
|
|
76
|
+
description="This cannot be undone."
|
|
77
|
+
actions={[
|
|
78
|
+
{
|
|
79
|
+
label: 'Delete',
|
|
80
|
+
color: 'destructive',
|
|
81
|
+
testID: 'top-action',
|
|
82
|
+
onPress: () => setResult('dialog'),
|
|
83
|
+
},
|
|
84
|
+
{ label: 'Cancel', color: 'cancel' },
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The mirror image: a sheet opened from inside an already-open dialog. */
|
|
92
|
+
function SheetOverDialog() {
|
|
93
|
+
const sheetRef = useRef<BottomSheetRef>(null);
|
|
94
|
+
const dialogControl = useDialogControl();
|
|
95
|
+
const [result, setResult] = useState('idle');
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<Button testID="open-first" onPress={() => dialogControl.open()}>
|
|
100
|
+
Open dialog
|
|
101
|
+
</Button>
|
|
102
|
+
<Result value={result} />
|
|
103
|
+
|
|
104
|
+
<Dialog control={dialogControl} testID="outer-dialog" title="Settings">
|
|
105
|
+
<Button
|
|
106
|
+
testID="open-second"
|
|
107
|
+
variant="secondary"
|
|
108
|
+
onPress={() => sheetRef.current?.present()}
|
|
109
|
+
>
|
|
110
|
+
Pick an option
|
|
111
|
+
</Button>
|
|
112
|
+
</Dialog>
|
|
113
|
+
|
|
114
|
+
<BottomSheet ref={sheetRef}>
|
|
115
|
+
<View style={{ padding: 24, gap: 12 }}>
|
|
116
|
+
<Text style={{ fontSize: 20, fontWeight: '700' }}>Options</Text>
|
|
117
|
+
<Button testID="top-action" variant="secondary" onPress={() => setResult('sheet')}>
|
|
118
|
+
Choose this
|
|
119
|
+
</Button>
|
|
120
|
+
</View>
|
|
121
|
+
</BottomSheet>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Two dialogs. The second must sit above the first. */
|
|
127
|
+
function DialogOverDialog() {
|
|
128
|
+
const first = useDialogControl();
|
|
129
|
+
const second = useDialogControl();
|
|
130
|
+
const [result, setResult] = useState('idle');
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<Button testID="open-first" onPress={() => first.open()}>
|
|
135
|
+
Open first dialog
|
|
136
|
+
</Button>
|
|
137
|
+
<Result value={result} />
|
|
138
|
+
|
|
139
|
+
<Dialog control={first} testID="first-dialog" title="First">
|
|
140
|
+
<Button testID="open-second" variant="secondary" onPress={() => second.open()}>
|
|
141
|
+
Open second dialog
|
|
142
|
+
</Button>
|
|
143
|
+
</Dialog>
|
|
144
|
+
|
|
145
|
+
<Dialog control={second} testID="second-dialog" title="Second">
|
|
146
|
+
<Button testID="top-action" variant="secondary" onPress={() => setResult('second')}>
|
|
147
|
+
Act
|
|
148
|
+
</Button>
|
|
149
|
+
</Dialog>
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A menu opened from inside a dialog. `Menu` sat on the `dropdown` rung of the
|
|
156
|
+
* scale (40/41) and the dialog on `overlay` (50/60), so the menu opened behind
|
|
157
|
+
* the dialog that launched it no matter what order they opened in.
|
|
158
|
+
*/
|
|
159
|
+
function MenuOverDialog() {
|
|
160
|
+
const control = useDialogControl();
|
|
161
|
+
const [result, setResult] = useState('idle');
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<Button testID="open-first" onPress={() => control.open()}>
|
|
166
|
+
Open dialog
|
|
167
|
+
</Button>
|
|
168
|
+
<Result value={result} />
|
|
169
|
+
|
|
170
|
+
<Dialog control={control} testID="menu-host-dialog" title="Filters">
|
|
171
|
+
<Menu>
|
|
172
|
+
<MenuTrigger label="Sort by">
|
|
173
|
+
{({ props }) => (
|
|
174
|
+
<Button {...props} testID="open-second" variant="secondary">
|
|
175
|
+
Sort by
|
|
176
|
+
</Button>
|
|
177
|
+
)}
|
|
178
|
+
</MenuTrigger>
|
|
179
|
+
<MenuContent>
|
|
180
|
+
<MenuItem testID="top-action" label="Newest" onPress={() => setResult('menu')} />
|
|
181
|
+
<MenuItem label="Oldest" onPress={() => setResult('menu-oldest')} />
|
|
182
|
+
</MenuContent>
|
|
183
|
+
</Menu>
|
|
184
|
+
</Dialog>
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const DialogOverSheetStory: Story = {
|
|
190
|
+
name: 'Dialog over sheet',
|
|
191
|
+
render: () => <DialogOverSheet />,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const SheetOverDialogStory: Story = {
|
|
195
|
+
name: 'Sheet over dialog',
|
|
196
|
+
render: () => <SheetOverDialog />,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const DialogOverDialogStory: Story = {
|
|
200
|
+
name: 'Dialog over dialog',
|
|
201
|
+
render: () => <DialogOverDialog />,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const MenuOverDialogStory: Story = {
|
|
205
|
+
name: 'Menu over dialog',
|
|
206
|
+
render: () => <MenuOverDialog />,
|
|
207
|
+
};
|
package/src/overlay/index.tsx
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
40
40
|
* dimming layer; do not re-implement either with raw `View`s.
|
|
41
41
|
*/
|
|
42
|
-
import { memo, type ReactNode } from 'react';
|
|
42
|
+
import { createContext, memo, useContext, useMemo, type ReactNode } from 'react';
|
|
43
43
|
import { BlurView } from 'expo-blur';
|
|
44
44
|
import {
|
|
45
45
|
Platform,
|
|
@@ -52,9 +52,28 @@ import {
|
|
|
52
52
|
import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
|
|
53
53
|
|
|
54
54
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
55
|
+
import { layerForRank, type OverlayLayer } from './stack';
|
|
56
|
+
import { useOverlayLayer } from './use-overlay-layer';
|
|
55
57
|
|
|
56
58
|
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The z-indices of the nearest enclosing `OverlayRoot`. Descendants that order
|
|
62
|
+
* themselves within a surface (a dialog's panel above its own backdrop) read
|
|
63
|
+
* this instead of picking their own numbers.
|
|
64
|
+
*
|
|
65
|
+
* The default is the first rank rather than 0, so a surface part rendered
|
|
66
|
+
* outside any `OverlayRoot` still lands in the overlay band instead of behind
|
|
67
|
+
* the app.
|
|
68
|
+
*/
|
|
69
|
+
const OverlayLayerContext = createContext<OverlayLayer>(layerForRank(1));
|
|
70
|
+
OverlayLayerContext.displayName = 'BloomOverlayLayerContext';
|
|
71
|
+
|
|
72
|
+
/** Z-indices of the enclosing overlay surface. See `OverlayRoot`. */
|
|
73
|
+
export function useOverlayLayerContext(): OverlayLayer {
|
|
74
|
+
return useContext(OverlayLayerContext);
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
/**
|
|
59
78
|
* One blur radius for every Bloom overlay. Surfaces differ in what they show,
|
|
60
79
|
* not in how the app behind them recedes.
|
|
@@ -74,19 +93,92 @@ export interface OverlayRootProps {
|
|
|
74
93
|
children?: ReactNode;
|
|
75
94
|
style?: StyleProp<ViewStyle>;
|
|
76
95
|
testID?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Opt OUT of the open-order stack and pin to a fixed depth. Only the toast
|
|
98
|
+
* layer does this — a notification has to stay visible over whatever is open,
|
|
99
|
+
* including a surface opened after it. Everything else must leave this unset
|
|
100
|
+
* so it stacks by open order; a hand-picked number here is precisely the bug
|
|
101
|
+
* `./stack.ts` exists to remove.
|
|
102
|
+
*/
|
|
103
|
+
zIndex?: number;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
/**
|
|
80
|
-
* Outermost node of a portaled surface.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
107
|
+
* Outermost node of a portaled surface. Three jobs:
|
|
108
|
+
*
|
|
109
|
+
* - Fills the viewport.
|
|
110
|
+
* - Re-enables pointer events for its own children while empty gaps stay
|
|
111
|
+
* click-through (`box-none`), so a surface that only covers part of the
|
|
112
|
+
* screen never steals clicks from the app behind it.
|
|
113
|
+
* - Takes this surface's place in the overlay stack, so a surface opened later
|
|
114
|
+
* paints above one opened earlier (see `./stack.ts`).
|
|
115
|
+
*
|
|
116
|
+
* Because the rank is taken on MOUNT, this must be rendered inside whatever
|
|
117
|
+
* guard makes the surface appear (`if (!isOpen) return null`), which is where
|
|
118
|
+
* every Bloom surface already puts it. Descendants that need to order
|
|
119
|
+
* themselves within the surface read `useOverlayLayerContext()`.
|
|
84
120
|
*/
|
|
85
|
-
export function OverlayRoot({ children, style, testID }: OverlayRootProps) {
|
|
121
|
+
export function OverlayRoot({ children, style, testID, zIndex }: OverlayRootProps) {
|
|
122
|
+
// Split into two components rather than branching on the hook: a pinned root
|
|
123
|
+
// must not CONSUME a rank either. The toast host is pinned and mounts for the
|
|
124
|
+
// whole life of the app, so holding a rank would keep the live set permanently
|
|
125
|
+
// non-empty — the counter would never reset and depths would climb for the
|
|
126
|
+
// rest of the session.
|
|
127
|
+
return zIndex === undefined ? (
|
|
128
|
+
<StackedOverlayRoot style={style} testID={testID}>
|
|
129
|
+
{children}
|
|
130
|
+
</StackedOverlayRoot>
|
|
131
|
+
) : (
|
|
132
|
+
<PinnedOverlayRoot zIndex={zIndex} style={style} testID={testID}>
|
|
133
|
+
{children}
|
|
134
|
+
</PinnedOverlayRoot>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function StackedOverlayRoot({ children, style, testID }: Omit<OverlayRootProps, 'zIndex'>) {
|
|
139
|
+
const layer = useOverlayLayer();
|
|
86
140
|
return (
|
|
87
|
-
<
|
|
141
|
+
<OverlayRootView layer={layer} style={style} testID={testID}>
|
|
88
142
|
{children}
|
|
89
|
-
</
|
|
143
|
+
</OverlayRootView>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function PinnedOverlayRoot({
|
|
148
|
+
children,
|
|
149
|
+
style,
|
|
150
|
+
testID,
|
|
151
|
+
zIndex,
|
|
152
|
+
}: OverlayRootProps & { zIndex: number }) {
|
|
153
|
+
// Outside the stack, so descendants must not read stack depths from it
|
|
154
|
+
// either — every slot is the pinned depth.
|
|
155
|
+
const layer = useMemo(
|
|
156
|
+
() => ({ root: zIndex, backdrop: zIndex, surface: zIndex }),
|
|
157
|
+
[zIndex],
|
|
158
|
+
);
|
|
159
|
+
return (
|
|
160
|
+
<OverlayRootView layer={layer} style={style} testID={testID}>
|
|
161
|
+
{children}
|
|
162
|
+
</OverlayRootView>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function OverlayRootView({
|
|
167
|
+
children,
|
|
168
|
+
style,
|
|
169
|
+
testID,
|
|
170
|
+
layer,
|
|
171
|
+
}: Omit<OverlayRootProps, 'zIndex'> & { layer: OverlayLayer }) {
|
|
172
|
+
return (
|
|
173
|
+
<OverlayLayerContext.Provider value={layer}>
|
|
174
|
+
<View
|
|
175
|
+
pointerEvents="box-none"
|
|
176
|
+
style={[styles.root, { zIndex: layer.root }, style]}
|
|
177
|
+
testID={testID}
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</View>
|
|
181
|
+
</OverlayLayerContext.Provider>
|
|
90
182
|
);
|
|
91
183
|
}
|
|
92
184
|
|
|
@@ -112,7 +204,10 @@ export interface BackdropProps {
|
|
|
112
204
|
/** Dim opacity, 0–1. */
|
|
113
205
|
dimOpacity?: number;
|
|
114
206
|
/**
|
|
115
|
-
* Geometry for the press target: insets,
|
|
207
|
+
* Geometry for the press target: insets, layout. NOT a z-index — where this
|
|
208
|
+
* surface sits relative to others is `OverlayRoot`'s call (see `./stack.ts`),
|
|
209
|
+
* and within the surface the panel is simply rendered after this. NOT opacity
|
|
210
|
+
* either —
|
|
116
211
|
* `backdrop-filter` samples nothing under an ancestor with `opacity < 1`
|
|
117
212
|
* (the group composites in isolation), so a fade applied here silently kills
|
|
118
213
|
* the blur. An `opacity` found in this style is redirected onto the layers;
|
|
@@ -124,16 +219,28 @@ export interface BackdropProps {
|
|
|
124
219
|
* express — the web dialog's CSS keyframes, for instance.
|
|
125
220
|
*/
|
|
126
221
|
layerStyle?: StyleProp<ViewStyle>;
|
|
127
|
-
/** Rendered ON TOP of the dim,
|
|
222
|
+
/** Rendered ON TOP of the dim, as a sibling of the press target (Dialog's panel does this). */
|
|
128
223
|
children?: ReactNode;
|
|
129
224
|
accessibilityLabel?: string;
|
|
130
225
|
testID?: string;
|
|
131
226
|
}
|
|
132
227
|
|
|
133
228
|
/**
|
|
134
|
-
* Full-bleed blur + dim that dismisses the surface when pressed.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
229
|
+
* Full-bleed blur + dim that dismisses the surface when pressed.
|
|
230
|
+
*
|
|
231
|
+
* The dismiss target is a hit box that fills this component and sits BEHIND
|
|
232
|
+
* `children`, never around them. It used to wrap them, which read fine on
|
|
233
|
+
* native but is invalid on web: the hit box carries `accessibilityRole="button"`,
|
|
234
|
+
* so react-native-web renders it as a real `<button>` — and every control inside
|
|
235
|
+
* any Bloom surface (a dialog's own buttons, a menu's rows) became a nested
|
|
236
|
+
* `<button>`. React reports that as a hydration error, and the nested control's
|
|
237
|
+
* activation behaviour is undefined per the HTML spec.
|
|
238
|
+
*
|
|
239
|
+
* Hit testing is unchanged: the hit box still covers the whole area, `children`
|
|
240
|
+
* render above it and take their own presses, and a press that lands on empty
|
|
241
|
+
* space falls through to the hit box. Layout styles passed via `style` stay on
|
|
242
|
+
* the outer box, so a caller that centres its panel with this component (the
|
|
243
|
+
* centred dialog) keeps doing so.
|
|
137
244
|
*/
|
|
138
245
|
export const Backdrop = memo(function Backdrop({
|
|
139
246
|
onPress,
|
|
@@ -175,36 +282,40 @@ export const Backdrop = memo(function Backdrop({
|
|
|
175
282
|
);
|
|
176
283
|
|
|
177
284
|
return (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
285
|
+
// `box-none` so this box never takes a press itself: the hit box below and
|
|
286
|
+
// `children` above are what receive them.
|
|
287
|
+
<View pointerEvents="box-none" style={[StyleSheet.absoluteFill, rootStyle]}>
|
|
288
|
+
<Pressable
|
|
289
|
+
pointerEvents="auto"
|
|
290
|
+
onPress={inert ? undefined : onPress}
|
|
291
|
+
disabled={inert}
|
|
292
|
+
// A dimming layer is not a focus stop on web: Escape and the panel's own
|
|
293
|
+
// controls are the keyboard paths out. It stays labelled for screen
|
|
294
|
+
// readers that surface it as the dismiss affordance.
|
|
295
|
+
focusable={false}
|
|
296
|
+
accessibilityRole={inert ? undefined : 'button'}
|
|
297
|
+
accessibilityLabel={inert ? undefined : accessibilityLabel}
|
|
298
|
+
testID={testID}
|
|
299
|
+
style={StyleSheet.absoluteFill}
|
|
300
|
+
>
|
|
301
|
+
{blurIntensity > 0 ? (
|
|
302
|
+
<AnimatedBlurView
|
|
303
|
+
intensity={blurIntensity}
|
|
304
|
+
tint={blurTint}
|
|
305
|
+
// Android's default blur is a no-op on many devices; this is the
|
|
306
|
+
// implementation that actually renders there.
|
|
307
|
+
experimentalBlurMethod="dimezisBlurView"
|
|
308
|
+
pointerEvents="none"
|
|
309
|
+
style={[StyleSheet.absoluteFill, layerStyle, blurFade]}
|
|
310
|
+
/>
|
|
311
|
+
) : null}
|
|
312
|
+
<Animated.View
|
|
198
313
|
pointerEvents="none"
|
|
199
|
-
style={[StyleSheet.absoluteFill, layerStyle,
|
|
314
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: resolvedDimColor }, layerStyle, dimFade]}
|
|
200
315
|
/>
|
|
201
|
-
|
|
202
|
-
<Animated.View
|
|
203
|
-
pointerEvents="none"
|
|
204
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: resolvedDimColor }, layerStyle, dimFade]}
|
|
205
|
-
/>
|
|
316
|
+
</Pressable>
|
|
206
317
|
{children}
|
|
207
|
-
</
|
|
318
|
+
</View>
|
|
208
319
|
);
|
|
209
320
|
});
|
|
210
321
|
|
|
@@ -224,3 +335,13 @@ const styles = StyleSheet.create({
|
|
|
224
335
|
bottom: 0,
|
|
225
336
|
},
|
|
226
337
|
});
|
|
338
|
+
|
|
339
|
+
export {
|
|
340
|
+
layerForRank,
|
|
341
|
+
OVERLAY_STACK_BAND,
|
|
342
|
+
OVERLAY_STACK_BASE,
|
|
343
|
+
OVERLAY_STACK_MAX_RANK,
|
|
344
|
+
TOAST_LAYER_Z,
|
|
345
|
+
type OverlayLayer,
|
|
346
|
+
} from './stack';
|
|
347
|
+
export { useOverlayLayer } from './use-overlay-layer';
|