@oxyhq/bloom 0.72.2 → 0.73.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/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/button/Button.js +104 -42
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +11 -3
- package/lib/commonjs/button/Button.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/menu/index.web.js +15 -11
- package/lib/commonjs/menu/index.web.js.map +1 -1
- package/lib/commonjs/overlay/index.js +133 -7
- 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/button/Button.js +104 -42
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button/Button.web.js +11 -3
- package/lib/module/button/Button.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/menu/index.web.js +16 -12
- package/lib/module/menu/index.web.js.map +1 -1
- package/lib/module/overlay/index.js +99 -8
- 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/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/types.d.ts +8 -0
- package/lib/typescript/commonjs/button/types.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/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts +31 -6
- 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/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/button/types.d.ts +8 -0
- package/lib/typescript/module/button/types.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/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts +31 -6
- 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__/Button.test.tsx +166 -1
- package/src/__tests__/Button.web.test.tsx +16 -0
- package/src/__tests__/ToastHostWebFork.test.ts +11 -3
- 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/button/Button.tsx +123 -40
- package/src/button/Button.web.tsx +11 -3
- package/src/button/types.ts +8 -0
- 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/menu/index.web.tsx +33 -22
- package/src/overlay/OverlayStacking.stories.tsx +207 -0
- package/src/overlay/index.tsx +114 -9
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAC;AAmElG,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AA87BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AA0K9D,eAAe,oBAAoB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.1",
|
|
4
|
+
"packageManager": "bun@1.3.14",
|
|
4
5
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
6
|
"main": "lib/commonjs/index.js",
|
|
6
7
|
"module": "lib/module/index.js",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Text } from 'react-native';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
3
|
import { render, fireEvent } from '@testing-library/react-native';
|
|
4
4
|
|
|
5
5
|
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
+
import { useTheme } from '../theme/use-theme';
|
|
7
|
+
import type { ThemeColors } from '../theme/types';
|
|
6
8
|
import { Button, PrimaryButton, SecondaryButton, IconButton, GhostButton, TextButton } from '../button';
|
|
7
9
|
|
|
8
10
|
function renderWithTheme(ui: React.ReactElement) {
|
|
@@ -13,6 +15,71 @@ function renderWithTheme(ui: React.ReactElement) {
|
|
|
13
15
|
);
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
type StyleEntry = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deep-flatten an RN `style` prop (nested arrays, `false`/`null` holes) into the
|
|
22
|
+
* list of style objects that actually reached the node — including the
|
|
23
|
+
* `{ $$css: true, className }` descriptor react-native-css merges in.
|
|
24
|
+
*/
|
|
25
|
+
function styleEntries(style: unknown, out: StyleEntry[] = []): StyleEntry[] {
|
|
26
|
+
if (!style || typeof style !== 'object') return out;
|
|
27
|
+
if (Array.isArray(style)) {
|
|
28
|
+
for (const entry of style) styleEntries(entry, out);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
out.push(style as StyleEntry);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Every style key/value that landed on a node, later entries winning. */
|
|
36
|
+
function resolvedStyle(style: unknown): StyleEntry {
|
|
37
|
+
return Object.assign({}, ...styleEntries(style)) as StyleEntry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The class tokens react-native-css accepted for a node, in arrival order. */
|
|
41
|
+
function classNamesOn(style: unknown): string[] {
|
|
42
|
+
return styleEntries(style)
|
|
43
|
+
.filter((entry) => entry.$$css === true && typeof entry.className === 'string')
|
|
44
|
+
.map((entry) => String(entry.className));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface HostNode {
|
|
48
|
+
type: string;
|
|
49
|
+
props: Record<string, unknown>;
|
|
50
|
+
children: Array<HostNode | string> | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isHostNode(value: unknown): value is HostNode {
|
|
54
|
+
return typeof value === 'object' && value !== null && typeof (value as HostNode).type === 'string';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Walk the HOST tree (composites are absent from `toJSON()`) for a testID. */
|
|
58
|
+
function findHost(node: unknown, testID: string): HostNode | null {
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
for (const child of node) {
|
|
61
|
+
const hit = findHost(child, testID);
|
|
62
|
+
if (hit) return hit;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!isHostNode(node)) return null;
|
|
67
|
+
if (node.props.testID === testID) return node;
|
|
68
|
+
return findHost(node.children, testID);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Read the live resolved theme colors the same way `Button` does. */
|
|
72
|
+
function captureThemeColors(): ThemeColors {
|
|
73
|
+
let captured: ThemeColors | undefined;
|
|
74
|
+
function Probe() {
|
|
75
|
+
captured = useTheme().colors;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
renderWithTheme(<Probe />);
|
|
79
|
+
if (!captured) throw new Error('theme probe never rendered');
|
|
80
|
+
return captured;
|
|
81
|
+
}
|
|
82
|
+
|
|
16
83
|
describe('Button', () => {
|
|
17
84
|
it('renders children as text', () => {
|
|
18
85
|
const { getByText } = renderWithTheme(<Button>Click me</Button>);
|
|
@@ -95,6 +162,104 @@ describe('Button', () => {
|
|
|
95
162
|
});
|
|
96
163
|
});
|
|
97
164
|
|
|
165
|
+
// Regression: the native button used to render its `Pressable` inside an
|
|
166
|
+
// unstyled `Animated.View` that existed only to hold the press-scale transform.
|
|
167
|
+
// That wrapper — not the pressable — was the flex child of whatever laid the
|
|
168
|
+
// button out, and it hugged its content, so `className="flex-1"` (and every
|
|
169
|
+
// other layout the caller expressed) applied inside a box that never grew. The
|
|
170
|
+
// web fork renders one real `<button>` and has always behaved. Nothing errored;
|
|
171
|
+
// consumers wrapped each Bloom `Button` in their own `<View>` to compensate.
|
|
172
|
+
describe('layout: the button IS the node its parent lays out', () => {
|
|
173
|
+
it('renders the pressable as its outermost node — no wrapper in between', () => {
|
|
174
|
+
const { toJSON } = renderWithTheme(
|
|
175
|
+
<View testID="host">
|
|
176
|
+
<Button testID="btn" className="flex-1">
|
|
177
|
+
Wide
|
|
178
|
+
</Button>
|
|
179
|
+
</View>,
|
|
180
|
+
);
|
|
181
|
+
const host = findHost(toJSON(), 'host');
|
|
182
|
+
expect(host).not.toBeNull();
|
|
183
|
+
const rendered = (host?.children ?? []).filter(isHostNode);
|
|
184
|
+
expect(rendered).toHaveLength(1);
|
|
185
|
+
// The single node the parent lays out is the pressable itself — the one
|
|
186
|
+
// carrying the a11y role, the handlers and the button's own box.
|
|
187
|
+
expect(rendered[0]?.props.testID).toBe('btn');
|
|
188
|
+
expect(rendered[0]?.props.accessibilityRole).toBe('button');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('lands the caller className on that same node, beside the button box', () => {
|
|
192
|
+
const { getByTestId } = renderWithTheme(
|
|
193
|
+
<Button testID="btn" className="flex-1">
|
|
194
|
+
Wide
|
|
195
|
+
</Button>,
|
|
196
|
+
);
|
|
197
|
+
const style = getByTestId('btn').props.style;
|
|
198
|
+
expect(classNamesOn(style)).toContain('flex-1');
|
|
199
|
+
// Same node, so a layout class and the button's visuals cannot diverge.
|
|
200
|
+
expect(resolvedStyle(style).borderRadius).toBe(999);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('applies the press-scale transform to that node too, not to a wrapper', () => {
|
|
204
|
+
const { getByTestId } = renderWithTheme(<Button testID="btn">Press</Button>);
|
|
205
|
+
expect(resolvedStyle(getByTestId('btn').props.style).transform).toBeDefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('keeps the caller style winning over the button box', () => {
|
|
209
|
+
const { getByTestId } = renderWithTheme(
|
|
210
|
+
<Button testID="btn" style={{ minHeight: 0, paddingHorizontal: 2 }}>
|
|
211
|
+
Tight
|
|
212
|
+
</Button>,
|
|
213
|
+
);
|
|
214
|
+
const style = resolvedStyle(getByTestId('btn').props.style);
|
|
215
|
+
expect(style.minHeight).toBe(0);
|
|
216
|
+
expect(style.paddingHorizontal).toBe(2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Regression: the icon variant used to get its chrome from a DEFAULT
|
|
220
|
+
// `className` that the caller's own `className` replaced outright, so
|
|
221
|
+
// `<IconButton className="flex-1" />` shipped transparent and borderless. It
|
|
222
|
+
// now comes from the same resolved tokens `Button.web.tsx` already used.
|
|
223
|
+
it('keeps the icon variant chrome when the caller passes a className', () => {
|
|
224
|
+
const colors = captureThemeColors();
|
|
225
|
+
const { getByTestId } = renderWithTheme(<IconButton testID="icon" className="flex-1" />);
|
|
226
|
+
const style = resolvedStyle(getByTestId('icon').props.style);
|
|
227
|
+
expect(style.backgroundColor).toBe(colors.background);
|
|
228
|
+
expect(style.borderColor).toBe(colors.border);
|
|
229
|
+
expect(style.borderWidth).toBe(1);
|
|
230
|
+
expect(classNamesOn(getByTestId('icon').props.style)).toContain('flex-1');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// `variant="text"` was reported as "shrinks horizontal padding on native but is
|
|
235
|
+
// byte-identical to ghost on web". The padding is real and DELIBERATE — `text`
|
|
236
|
+
// (and its web alias `link`) is a compact inline affordance, `ghost` is a
|
|
237
|
+
// full-size button without a background — and web applies exactly the same 4/8
|
|
238
|
+
// override in `Button.web.tsx`'s `containerStyle`; only the variant COLOR table
|
|
239
|
+
// entry is shared with ghost there. The one genuine divergence left is
|
|
240
|
+
// `minHeight`, which web clears for text/link and native deliberately keeps: a
|
|
241
|
+
// finger needs the target, a cursor does not.
|
|
242
|
+
describe('variant="text" geometry', () => {
|
|
243
|
+
it('is a compact affordance, not a ghost button', () => {
|
|
244
|
+
const { getByTestId } = renderWithTheme(<TextButton testID="txt">Text</TextButton>);
|
|
245
|
+
const style = resolvedStyle(getByTestId('txt').props.style);
|
|
246
|
+
expect(style.paddingVertical).toBe(4);
|
|
247
|
+
expect(style.paddingHorizontal).toBe(8);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('ghost keeps the full size-config padding', () => {
|
|
251
|
+
const { getByTestId } = renderWithTheme(<GhostButton testID="ghost">Ghost</GhostButton>);
|
|
252
|
+
const style = resolvedStyle(getByTestId('ghost').props.style);
|
|
253
|
+
expect(style.paddingVertical).toBe(8);
|
|
254
|
+
expect(style.paddingHorizontal).toBe(16);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('keeps the size-config minHeight so the touch target survives', () => {
|
|
258
|
+
const { getByTestId } = renderWithTheme(<TextButton testID="txt">Text</TextButton>);
|
|
259
|
+
expect(resolvedStyle(getByTestId('txt').props.style).minHeight).toBe(40);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
98
263
|
describe('Button variants', () => {
|
|
99
264
|
it('PrimaryButton renders without crashing', () => {
|
|
100
265
|
const { getByText } = renderWithTheme(
|
|
@@ -129,6 +129,22 @@ describe('Button.web', () => {
|
|
|
129
129
|
expect(btn).toHaveClass('custom');
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
// Parity with the native fork's "the button IS the node its parent lays out"
|
|
133
|
+
// suite in `Button.test.tsx`. Both platforms render ONE node, so a caller's
|
|
134
|
+
// layout class reaches the box the parent actually lays out; a wrapper element
|
|
135
|
+
// added here would scope it away exactly the way native's `Animated.View` did.
|
|
136
|
+
it('renders the classed button as the outermost node — no wrapper element', () => {
|
|
137
|
+
const c = mount(
|
|
138
|
+
<div data-testid="host">
|
|
139
|
+
<Button className="flex-1">Wide</Button>
|
|
140
|
+
</div>,
|
|
141
|
+
);
|
|
142
|
+
const host = c.querySelector('[data-testid="host"]');
|
|
143
|
+
expect(host?.children).toHaveLength(1);
|
|
144
|
+
expect(host?.firstElementChild?.tagName).toBe('BUTTON');
|
|
145
|
+
expect(host?.firstElementChild).toHaveClass('flex-1');
|
|
146
|
+
});
|
|
147
|
+
|
|
132
148
|
it('applies aria-label from accessibilityLabel', () => {
|
|
133
149
|
const c = mount(<Button accessibilityLabel="Save changes">Save</Button>);
|
|
134
150
|
expect(getByLabelText(c, 'Save changes')).toBeTruthy();
|
|
@@ -52,9 +52,17 @@ describe('ToastHost platform split', () => {
|
|
|
52
52
|
expect(host).not.toMatch(/'fixed' as/);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it('web/default host
|
|
56
|
-
const host =
|
|
57
|
-
|
|
55
|
+
it('web/default host pins toasts above the whole overlay stack', () => {
|
|
56
|
+
const host = code('toast/ToastHost.tsx');
|
|
57
|
+
// A toast is a notification, not a modal surface: it must stay visible over
|
|
58
|
+
// whatever is open, including a surface opened AFTER it. So it opts out of
|
|
59
|
+
// the open-order stack via `OverlayRoot`'s `zIndex` prop rather than taking
|
|
60
|
+
// a rank (see `src/overlay/stack.ts`).
|
|
61
|
+
expect(host).toMatch(/<OverlayRoot zIndex=\{TOAST_LAYER_Z\}/);
|
|
62
|
+
// And it must not ALSO carry a hand-picked depth in its stylesheet: a fixed
|
|
63
|
+
// number there is exactly what left toasts stranded behind an open bottom
|
|
64
|
+
// sheet, and it would fight the prop above.
|
|
65
|
+
expect(host).not.toMatch(/zIndex: Z_INDEX\./);
|
|
58
66
|
});
|
|
59
67
|
|
|
60
68
|
it('native host imports neither react-native-screens nor the portal (D6)', () => {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The overlay stacking rule: a surface opened LATER paints above one opened
|
|
7
|
+
* earlier.
|
|
8
|
+
*
|
|
9
|
+
* Jest cannot see the bug this guards — the losing surface renders perfectly
|
|
10
|
+
* and its styles are all valid; it is simply painted underneath. So these tests
|
|
11
|
+
* pin the two halves Jest CAN decide:
|
|
12
|
+
*
|
|
13
|
+
* 1. the authority in `src/overlay/stack.ts` hands out ascending depths, and
|
|
14
|
+
* keeps doing so across the interleavings that actually occur;
|
|
15
|
+
* 2. every overlay surface in `src/` gets its depth FROM that authority
|
|
16
|
+
* rather than from a constant of its own — the property whose absence
|
|
17
|
+
* caused the bug in the first place.
|
|
18
|
+
*
|
|
19
|
+
* The hit test that proves the user-visible outcome lives in
|
|
20
|
+
* `scripts/verify-overlay-stacking.mjs`, driven against a real browser.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { createElement } from 'react';
|
|
26
|
+
import { act } from 'react';
|
|
27
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
28
|
+
|
|
29
|
+
// The point of the DOM half is what react-native-web actually emits, so it runs
|
|
30
|
+
// against the REAL RNW — the repo-wide `react-native` mock renders no host
|
|
31
|
+
// elements and would make every depth assertion below vacuous.
|
|
32
|
+
jest.mock('react-native', () => jest.requireActual('react-native-web'));
|
|
33
|
+
|
|
34
|
+
import { OverlayRoot } from '../overlay';
|
|
35
|
+
import {
|
|
36
|
+
acquireOverlayRank,
|
|
37
|
+
layerForRank,
|
|
38
|
+
OVERLAY_STACK_BASE,
|
|
39
|
+
OVERLAY_STACK_MAX_RANK,
|
|
40
|
+
registerOverlayRank,
|
|
41
|
+
releaseOverlayRank,
|
|
42
|
+
resetOverlayStack,
|
|
43
|
+
TOAST_LAYER_Z,
|
|
44
|
+
} from '../overlay/stack';
|
|
45
|
+
|
|
46
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
resetOverlayStack();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('the stacking authority', () => {
|
|
53
|
+
it('gives a later opener a higher depth than an earlier one', () => {
|
|
54
|
+
const first = layerForRank(acquireOverlayRank());
|
|
55
|
+
const second = layerForRank(acquireOverlayRank());
|
|
56
|
+
const third = layerForRank(acquireOverlayRank());
|
|
57
|
+
|
|
58
|
+
expect(second.root).toBeGreaterThan(first.root);
|
|
59
|
+
expect(third.root).toBeGreaterThan(second.root);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('puts a surface above its OWN backdrop without reaching the next surface', () => {
|
|
63
|
+
const lower = layerForRank(acquireOverlayRank());
|
|
64
|
+
const upper = layerForRank(acquireOverlayRank());
|
|
65
|
+
|
|
66
|
+
expect(lower.surface).toBeGreaterThan(lower.backdrop);
|
|
67
|
+
// The band is what keeps a surface's own parts from colliding with the
|
|
68
|
+
// floor of the surface above it.
|
|
69
|
+
expect(lower.surface).toBeLessThan(upper.backdrop);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('keeps ordering when surfaces close out of order', () => {
|
|
73
|
+
// A opens, B opens, A closes, C opens. C is newest and must outrank B.
|
|
74
|
+
const a = acquireOverlayRank();
|
|
75
|
+
registerOverlayRank(a);
|
|
76
|
+
const b = acquireOverlayRank();
|
|
77
|
+
registerOverlayRank(b);
|
|
78
|
+
|
|
79
|
+
releaseOverlayRank(a);
|
|
80
|
+
|
|
81
|
+
const c = acquireOverlayRank();
|
|
82
|
+
registerOverlayRank(c);
|
|
83
|
+
|
|
84
|
+
expect(layerForRank(c).root).toBeGreaterThan(layerForRank(b).root);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('starts over once every surface has closed, so depths stay small', () => {
|
|
88
|
+
const a = acquireOverlayRank();
|
|
89
|
+
registerOverlayRank(a);
|
|
90
|
+
releaseOverlayRank(a);
|
|
91
|
+
|
|
92
|
+
expect(layerForRank(acquireOverlayRank()).root).toBe(layerForRank(a).root);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('cannot lose a rank handed out but not yet registered', () => {
|
|
96
|
+
// The one interleaving the reset could otherwise break: a surface takes a
|
|
97
|
+
// rank during render, and before its effect registers it the last open
|
|
98
|
+
// surface unmounts and resets the counter. The newcomer must still land
|
|
99
|
+
// ABOVE the one that is on its way up.
|
|
100
|
+
const open = acquireOverlayRank();
|
|
101
|
+
registerOverlayRank(open);
|
|
102
|
+
|
|
103
|
+
const rendering = acquireOverlayRank(); // taken, effect not run yet
|
|
104
|
+
releaseOverlayRank(open); // live set empties -> reset
|
|
105
|
+
registerOverlayRank(rendering); // effect finally runs
|
|
106
|
+
|
|
107
|
+
const next = acquireOverlayRank();
|
|
108
|
+
expect(next).toBeGreaterThan(rendering);
|
|
109
|
+
expect(layerForRank(next).root).toBeGreaterThan(layerForRank(rendering).root);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('saturates instead of climbing into the toast layer', () => {
|
|
113
|
+
const runaway = layerForRank(OVERLAY_STACK_MAX_RANK + 5_000);
|
|
114
|
+
expect(runaway.surface).toBeLessThan(TOAST_LAYER_Z);
|
|
115
|
+
expect(runaway.root).toBe(layerForRank(OVERLAY_STACK_MAX_RANK).root);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('keeps every rank below the toast layer', () => {
|
|
119
|
+
// Toasts are notifications, not modal surfaces: they stay visible over
|
|
120
|
+
// whatever is open, including a surface opened after them.
|
|
121
|
+
expect(layerForRank(1).root).toBeGreaterThanOrEqual(OVERLAY_STACK_BASE);
|
|
122
|
+
expect(layerForRank(OVERLAY_STACK_MAX_RANK).surface).toBeLessThan(TOAST_LAYER_Z);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('OverlayRoot', () => {
|
|
127
|
+
function render(node: ReturnType<typeof createElement>): {
|
|
128
|
+
root: Root;
|
|
129
|
+
container: HTMLElement;
|
|
130
|
+
} {
|
|
131
|
+
const container = document.createElement('div');
|
|
132
|
+
document.body.appendChild(container);
|
|
133
|
+
const root = createRoot(container);
|
|
134
|
+
act(() => {
|
|
135
|
+
root.render(node);
|
|
136
|
+
});
|
|
137
|
+
return { root, container };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const depthOf = (container: HTMLElement, testID: string) => {
|
|
141
|
+
const el = container.querySelector<HTMLElement>(`[data-testid="${testID}"]`);
|
|
142
|
+
if (!el) throw new Error(`no element with testID ${testID}`);
|
|
143
|
+
return Number(getComputedStyle(el).zIndex);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
it('paints a surface mounted later above one mounted earlier', () => {
|
|
147
|
+
const first = render(createElement(OverlayRoot, { testID: 'first', children: null }));
|
|
148
|
+
const second = render(createElement(OverlayRoot, { testID: 'second', children: null }));
|
|
149
|
+
|
|
150
|
+
const firstZ = depthOf(first.container, 'first');
|
|
151
|
+
const secondZ = depthOf(second.container, 'second');
|
|
152
|
+
|
|
153
|
+
// Vacuity floor: both must have emitted a real depth, or "second > first"
|
|
154
|
+
// would be comparing two NaNs into a silent pass.
|
|
155
|
+
expect(Number.isFinite(firstZ)).toBe(true);
|
|
156
|
+
expect(Number.isFinite(secondZ)).toBe(true);
|
|
157
|
+
expect(secondZ).toBeGreaterThan(firstZ);
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
first.root.unmount();
|
|
161
|
+
second.root.unmount();
|
|
162
|
+
});
|
|
163
|
+
first.container.remove();
|
|
164
|
+
second.container.remove();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('pins to the given depth, and takes no rank, when told to', () => {
|
|
168
|
+
const pinned = render(
|
|
169
|
+
createElement(OverlayRoot, { testID: 'pinned', zIndex: TOAST_LAYER_Z, children: null }),
|
|
170
|
+
);
|
|
171
|
+
expect(depthOf(pinned.container, 'pinned')).toBe(TOAST_LAYER_Z);
|
|
172
|
+
|
|
173
|
+
// Having opted out, it must not have consumed a rank either — the next
|
|
174
|
+
// surface to open is still the FIRST one in the stack.
|
|
175
|
+
const ranked = render(createElement(OverlayRoot, { testID: 'ranked', children: null }));
|
|
176
|
+
expect(depthOf(ranked.container, 'ranked')).toBe(layerForRank(1).root);
|
|
177
|
+
|
|
178
|
+
act(() => {
|
|
179
|
+
pinned.root.unmount();
|
|
180
|
+
ranked.root.unmount();
|
|
181
|
+
});
|
|
182
|
+
pinned.container.remove();
|
|
183
|
+
ranked.container.remove();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('no surface picks its own depth', () => {
|
|
188
|
+
const SRC = path.join(__dirname, '..');
|
|
189
|
+
const read = (rel: string) => readFileSync(path.join(SRC, rel), 'utf8');
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Every file in `src/` that presents an overlay surface. Enumerated
|
|
193
|
+
* explicitly rather than globbed: a glob that silently stops matching is the
|
|
194
|
+
* shape of check that passes forever while covering nothing.
|
|
195
|
+
*/
|
|
196
|
+
const SURFACES = [
|
|
197
|
+
'dialog/Dialog.tsx',
|
|
198
|
+
'dialog/Dialog.web.tsx',
|
|
199
|
+
'bottom-sheet/index.web.tsx',
|
|
200
|
+
'menu/index.web.tsx',
|
|
201
|
+
'select/index.web.tsx',
|
|
202
|
+
'context-menu/index.web.tsx',
|
|
203
|
+
'popover/index.web.tsx',
|
|
204
|
+
'tooltip/index.tsx',
|
|
205
|
+
'zoomable-image-gallery/ZoomableImageGallery.tsx',
|
|
206
|
+
'prompt-input/PromptInput.tsx',
|
|
207
|
+
'avatar-group/AvatarGroup.web.tsx',
|
|
208
|
+
'toast/ToastHost.tsx',
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
/** Strips comments, so prose ABOUT the old constants never trips the scan. */
|
|
212
|
+
const code = (rel: string) =>
|
|
213
|
+
read(rel)
|
|
214
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
215
|
+
.replace(/^\s*\/\/.*$/gm, '')
|
|
216
|
+
.replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, '');
|
|
217
|
+
|
|
218
|
+
it.each(SURFACES)('%s claims no overlay-ordering constant', (rel) => {
|
|
219
|
+
const body = code(rel);
|
|
220
|
+
|
|
221
|
+
// The rungs that used to decide which SURFACE was on top. Their absence is
|
|
222
|
+
// the property that makes open order the only authority.
|
|
223
|
+
expect(body).not.toMatch(
|
|
224
|
+
/Z_INDEX\.(overlayBackdrop|overlaySurface|dropdownSurface|tooltip|toast|fullscreen|fullscreenControl|portalRoot)\b/,
|
|
225
|
+
);
|
|
226
|
+
expect(body).not.toMatch(/createOverlayZIndex|createDropdownZIndex|Z_INDEX_LAYER_STEP/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('the scale no longer offers an overlay-ordering rung to reach for', () => {
|
|
230
|
+
const scale = code('styles/z-index.ts');
|
|
231
|
+
for (const gone of [
|
|
232
|
+
'overlayBackdrop',
|
|
233
|
+
'overlaySurface',
|
|
234
|
+
'dropdownSurface',
|
|
235
|
+
'tooltip',
|
|
236
|
+
'toast',
|
|
237
|
+
'fullscreen',
|
|
238
|
+
'fullscreenControl',
|
|
239
|
+
'createOverlayZIndex',
|
|
240
|
+
'createDropdownZIndex',
|
|
241
|
+
'Z_INDEX_LAYER_STEP',
|
|
242
|
+
]) {
|
|
243
|
+
expect(scale).not.toContain(gone);
|
|
244
|
+
}
|
|
245
|
+
// Vacuity floor: the module must still export the within-context scale, or
|
|
246
|
+
// the loop above would pass against an empty file.
|
|
247
|
+
expect(scale).toMatch(/export const Z_INDEX = \{/);
|
|
248
|
+
expect(scale).toMatch(/portalRoot: 999999/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('the WEB tooltip is deliberately NOT in this list', () => {
|
|
252
|
+
// It is the one tooltip that is not portaled: the bubble is absolutely
|
|
253
|
+
// positioned against its own trigger and renders inline, so it cannot be
|
|
254
|
+
// lifted over another surface and has no rank to take. The NATIVE tooltip
|
|
255
|
+
// does portal, and IS in the list above. If this ever starts portaling,
|
|
256
|
+
// move it into `SURFACES`.
|
|
257
|
+
expect(code('tooltip/index.web.tsx')).not.toMatch(/from '\.\.\/portal/);
|
|
258
|
+
expect(code('tooltip/index.tsx')).toMatch(/from '\.\.\/portal/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('every surface routes its depth through OverlayRoot', () => {
|
|
262
|
+
// `Dialog.web`'s side placement renders `SheetSurface`, which mounts its
|
|
263
|
+
// own `OverlayRoot`; the centred branch mounts one directly. Both files are
|
|
264
|
+
// covered by the membership check below.
|
|
265
|
+
const missing = SURFACES.filter((rel) => !code(rel).includes('OverlayRoot'));
|
|
266
|
+
expect(missing).toEqual([]);
|
|
267
|
+
// Vacuity floor on the list itself.
|
|
268
|
+
expect(SURFACES.length).toBeGreaterThanOrEqual(12);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { OverlayRoot } from '../overlay';
|
|
4
5
|
import { Portal } from '../portal';
|
|
5
|
-
import { Z_INDEX } from '../styles/z-index';
|
|
6
6
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
7
7
|
import { UserHoverCard } from '../user-hover-card';
|
|
8
8
|
import {
|
|
@@ -97,35 +97,40 @@ const AvatarGroupWebComponent: React.FC<AvatarGroupProps> = (props) => {
|
|
|
97
97
|
<AvatarGroupBase {...props} hoverHandlers={hoverHandlers} />
|
|
98
98
|
{hoverCard && hover && (
|
|
99
99
|
<Portal>
|
|
100
|
-
{/*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
100
|
+
{/* The hover card stacks by when it opened, like every other Bloom
|
|
101
|
+
overlay (`src/overlay/stack.ts`). `box-none` keeps the area
|
|
102
|
+
around it click-through. */}
|
|
103
|
+
<OverlayRoot>
|
|
104
|
+
{/*
|
|
105
|
+
A Pressable hosts the hover-bridge: `onHoverIn`/`onHoverOut` are
|
|
106
|
+
typed in React Native (and map to mouseenter/mouseleave on
|
|
107
|
+
react-native-web), so the card stays open while the cursor is over
|
|
108
|
+
it without any web-only DOM-prop type suppression.
|
|
109
|
+
*/}
|
|
110
|
+
<Pressable
|
|
111
|
+
onHoverIn={cancelClose}
|
|
112
|
+
onHoverOut={scheduleClose}
|
|
113
|
+
accessibilityRole="none"
|
|
114
|
+
pointerEvents="auto"
|
|
115
|
+
style={[
|
|
116
|
+
webStyles.floating,
|
|
117
|
+
{ top: hover.top, left: hover.left, width: CARD_WIDTH },
|
|
118
|
+
]}
|
|
119
|
+
>
|
|
120
|
+
<UserHoverCard
|
|
121
|
+
avatar={hover.item.uri ?? undefined}
|
|
122
|
+
displayName={getItemName(hover.item)}
|
|
123
|
+
username={hover.item.username}
|
|
124
|
+
onPressProfile={
|
|
125
|
+
onPressItem
|
|
126
|
+
? () => onPressItem(hover.item, hover.index)
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
action={renderItemAction?.(hover.item, hover.index)}
|
|
130
|
+
style={webStyles.card}
|
|
131
|
+
/>
|
|
132
|
+
</Pressable>
|
|
133
|
+
</OverlayRoot>
|
|
129
134
|
</Portal>
|
|
130
135
|
)}
|
|
131
136
|
</>
|
|
@@ -135,7 +140,6 @@ const AvatarGroupWebComponent: React.FC<AvatarGroupProps> = (props) => {
|
|
|
135
140
|
const webStyles = StyleSheet.create({
|
|
136
141
|
floating: {
|
|
137
142
|
position: WEB_POSITION_FIXED,
|
|
138
|
-
zIndex: Z_INDEX.tooltip,
|
|
139
143
|
},
|
|
140
144
|
card: {
|
|
141
145
|
width: CARD_WIDTH,
|
|
@@ -4,7 +4,6 @@ import { StyleSheet } from 'react-native';
|
|
|
4
4
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
5
5
|
import { OverlayRoot } from '../overlay';
|
|
6
6
|
import { Portal } from '../portal/index.web';
|
|
7
|
-
import { Z_INDEX } from '../styles/z-index';
|
|
8
7
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
9
8
|
import {
|
|
10
9
|
BottomSheetBase,
|
|
@@ -54,16 +53,22 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
54
53
|
BottomSheet.displayName = 'BottomSheet';
|
|
55
54
|
|
|
56
55
|
const webStyles = StyleSheet.create({
|
|
56
|
+
// Fixed, full-viewport box the sheet's `StyleSheet.absoluteFill` body
|
|
57
|
+
// anchors to. The pointer-events opt-in and the STACKING both live in
|
|
58
|
+
// `OverlayRoot`.
|
|
59
|
+
//
|
|
60
|
+
// This used to also pin `zIndex: Z_INDEX.portalRoot` (999999) here. Inside
|
|
61
|
+
// the portal root that number does not mean "the portal layer" — it means
|
|
62
|
+
// "above every other portaled surface", which is how a confirm dialog
|
|
63
|
+
// opened from inside an open sheet ended up rendering underneath it,
|
|
64
|
+
// unreachable, no matter which one opened last. Stacking is the overlay
|
|
65
|
+
// stack's job now (`src/overlay/stack.ts`); do not reintroduce a constant.
|
|
57
66
|
rootView: {
|
|
58
|
-
// Fixed, full-viewport box the sheet's `StyleSheet.absoluteFill` body
|
|
59
|
-
// anchors to. The pointer-events opt-in lives in `OverlayRoot`; this
|
|
60
|
-
// only adds the stacking context.
|
|
61
67
|
position: WEB_POSITION_FIXED,
|
|
62
68
|
top: 0,
|
|
63
69
|
left: 0,
|
|
64
70
|
right: 0,
|
|
65
71
|
bottom: 0,
|
|
66
|
-
zIndex: Z_INDEX.portalRoot,
|
|
67
72
|
},
|
|
68
73
|
});
|
|
69
74
|
|