@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay stacking authority.
|
|
3
|
+
*
|
|
4
|
+
* ## The rule
|
|
5
|
+
*
|
|
6
|
+
* An overlay opened LATER paints above one opened earlier. That is the whole
|
|
7
|
+
* contract, and it is decided here, once, for every Bloom overlay surface —
|
|
8
|
+
* never by a per-component constant.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this module exists
|
|
11
|
+
*
|
|
12
|
+
* Stacking used to be a set of hand-picked rungs on a shared scale: menus at
|
|
13
|
+
* 40, dialogs at 50/60, tooltips at 70, sheets at 999999. Because those numbers
|
|
14
|
+
* are fixed per COMPONENT KIND, the answer to "which surface is on top" was
|
|
15
|
+
* decided by what each surface WAS rather than by when the user opened it — so
|
|
16
|
+
* some pairings were permanently inverted no matter the order:
|
|
17
|
+
*
|
|
18
|
+
* - A confirm dialog (50/60) opened from inside an open bottom sheet (999999)
|
|
19
|
+
* rendered fully, interactively, and completely underneath the sheet. It
|
|
20
|
+
* could be neither pressed nor dismissed.
|
|
21
|
+
* - A menu (40/41) opened from inside a dialog (50/60) landed behind the very
|
|
22
|
+
* dialog that launched it.
|
|
23
|
+
*
|
|
24
|
+
* Both failed silently: correct markup, correct styles, nothing in the console.
|
|
25
|
+
* Only a hit test in a real browser tells the difference, which is why
|
|
26
|
+
* `scripts/verify-overlay-stacking.mjs` exists alongside the unit tests here.
|
|
27
|
+
*
|
|
28
|
+
* ## How a rank is acquired
|
|
29
|
+
*
|
|
30
|
+
* `useOverlayLayer()` takes a rank on MOUNT and releases it on unmount, so the
|
|
31
|
+
* caller must be a component that mounts when the surface OPENS — which is how
|
|
32
|
+
* every Bloom surface is already built (`Dialog.web` returns null while closed;
|
|
33
|
+
* `BottomSheetBase` returns null until `rendered`). Acquiring per mount, in a
|
|
34
|
+
* `useState` initializer, is what makes the rank correct on the surface's FIRST
|
|
35
|
+
* paint: an effect-assigned z would leave one frame at the wrong depth, which
|
|
36
|
+
* on a 200ms fade-in is a visible flash of the surface behind its neighbour.
|
|
37
|
+
*
|
|
38
|
+
* It also keeps the acquisition out of any position the React Compiler may
|
|
39
|
+
* memoize. A counter read during render is exactly the external-mutable-state
|
|
40
|
+
* hazard the compiler mangles; a state initializer is not memoizable that way.
|
|
41
|
+
* (React StrictMode double-invokes initializers in development, so a rank can
|
|
42
|
+
* be consumed and dropped. That is harmless: only the ORDER of the numbers
|
|
43
|
+
* matters, never their density.)
|
|
44
|
+
*
|
|
45
|
+
* ## Toasts are deliberately not in this stack
|
|
46
|
+
*
|
|
47
|
+
* A toast is a notification, not a modal surface: it must stay visible over
|
|
48
|
+
* whatever is open, including a surface opened after it. It is pinned above the
|
|
49
|
+
* whole stack at `TOAST_LAYER_Z` instead of taking a rank. (Under the old
|
|
50
|
+
* scale a toast at 80 was already lost behind any open bottom sheet at 999999.)
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* First z-index handed to an overlay. Comfortably above app content while
|
|
55
|
+
* staying far below `TOAST_LAYER_Z`.
|
|
56
|
+
*/
|
|
57
|
+
export const OVERLAY_STACK_BASE = 1000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Z-index distance between consecutive overlays. A surface owns its whole band,
|
|
61
|
+
* so it can order its own parts (backdrop, panel, a floating control) without
|
|
62
|
+
* ever reaching the next surface's floor.
|
|
63
|
+
*/
|
|
64
|
+
export const OVERLAY_STACK_BAND = 10;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ceiling on simultaneously open overlays. Beyond this, ranks saturate and the
|
|
68
|
+
* topmost surfaces tie (falling back to DOM order) rather than climbing into
|
|
69
|
+
* the toast layer. Nothing in the ecosystem stacks remotely this deep; the
|
|
70
|
+
* clamp exists so a runaway caller degrades instead of breaking the toast
|
|
71
|
+
* layer's guarantee.
|
|
72
|
+
*/
|
|
73
|
+
export const OVERLAY_STACK_MAX_RANK = 500;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The toast layer, pinned above every overlay rank (see the note above on why
|
|
77
|
+
* toasts are not part of the stack).
|
|
78
|
+
*/
|
|
79
|
+
export const TOAST_LAYER_Z =
|
|
80
|
+
OVERLAY_STACK_BASE + OVERLAY_STACK_BAND * (OVERLAY_STACK_MAX_RANK + 1);
|
|
81
|
+
|
|
82
|
+
/** The z-indices one overlay surface may use, all within its own band. */
|
|
83
|
+
export interface OverlayLayer {
|
|
84
|
+
/** The surface's outermost node — everything it renders is inside this. */
|
|
85
|
+
root: number;
|
|
86
|
+
/** Its dimming layer. */
|
|
87
|
+
backdrop: number;
|
|
88
|
+
/** Its panel, above its own backdrop. */
|
|
89
|
+
surface: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Registry state.
|
|
94
|
+
*
|
|
95
|
+
* `sequence` only ever moves forward while anything is open, which is what
|
|
96
|
+
* guarantees the ordering. It resets once the last overlay closes so the
|
|
97
|
+
* numbers stay small over a long session.
|
|
98
|
+
*/
|
|
99
|
+
interface Registry {
|
|
100
|
+
sequence: number;
|
|
101
|
+
live: Set<number>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
declare global {
|
|
105
|
+
// eslint-disable-next-line no-var
|
|
106
|
+
var __oxyhq_bloom_overlay_stack__: Registry | undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `globalThis`-anchored, for the same reason as the portal group and the theme
|
|
111
|
+
* context: `exports` ships a `react-native` → `src` condition beside the
|
|
112
|
+
* `lib/module` and `lib/commonjs` forks, and overlay surfaces are imported
|
|
113
|
+
* cross-subpath (`./dialog`, `./bottom-sheet`, `./menu`, `./select`, …). A
|
|
114
|
+
* bundler can resolve those through different conditions, and two physical
|
|
115
|
+
* copies of this module would each run their own counter — handing out
|
|
116
|
+
* colliding ranks and reintroducing exactly the bug this module removes.
|
|
117
|
+
*/
|
|
118
|
+
function registry(): Registry {
|
|
119
|
+
globalThis.__oxyhq_bloom_overlay_stack__ ??= { sequence: 0, live: new Set() };
|
|
120
|
+
return globalThis.__oxyhq_bloom_overlay_stack__;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Z-indices for a given rank. Exported for tests and for the native surfaces. */
|
|
124
|
+
export function layerForRank(rank: number): OverlayLayer {
|
|
125
|
+
const clamped = Math.min(Math.max(rank, 1), OVERLAY_STACK_MAX_RANK);
|
|
126
|
+
const root = OVERLAY_STACK_BASE + OVERLAY_STACK_BAND * clamped;
|
|
127
|
+
return { root, backdrop: root, surface: root + 1 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Take the next rank. Exported for tests; components use `useOverlayLayer`.
|
|
132
|
+
*/
|
|
133
|
+
export function acquireOverlayRank(): number {
|
|
134
|
+
const reg = registry();
|
|
135
|
+
reg.sequence += 1;
|
|
136
|
+
return reg.sequence;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mark a rank as live. Called from an effect, which React balances against
|
|
141
|
+
* `releaseOverlayRank` even under StrictMode's mount/unmount/remount.
|
|
142
|
+
*
|
|
143
|
+
* It also re-floors `sequence` to at least this rank. That closes the only race
|
|
144
|
+
* the reset below could otherwise open: a surface takes rank N during render,
|
|
145
|
+
* and before its effect runs, the last previously-open surface unmounts and
|
|
146
|
+
* resets the counter to 0 — the next surface would then take rank 1 and sit
|
|
147
|
+
* BELOW the one still coming up at N. Re-flooring on register makes the reset
|
|
148
|
+
* unable to lose a rank that has been handed out.
|
|
149
|
+
*/
|
|
150
|
+
export function registerOverlayRank(rank: number): void {
|
|
151
|
+
const reg = registry();
|
|
152
|
+
reg.live.add(rank);
|
|
153
|
+
if (rank > reg.sequence) reg.sequence = rank;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Release a rank; resets the counter once nothing is open. */
|
|
157
|
+
export function releaseOverlayRank(rank: number): void {
|
|
158
|
+
const reg = registry();
|
|
159
|
+
reg.live.delete(rank);
|
|
160
|
+
if (reg.live.size === 0) reg.sequence = 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Test seam — drops all registry state. */
|
|
164
|
+
export function resetOverlayStack(): void {
|
|
165
|
+
globalThis.__oxyhq_bloom_overlay_stack__ = { sequence: 0, live: new Set() };
|
|
166
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useOverlayLayer` — the hook every Bloom overlay surface stacks with.
|
|
3
|
+
*
|
|
4
|
+
* See `./stack.ts` for the rule and the reasoning. The contract for callers is
|
|
5
|
+
* one line: **call this from a component that mounts when the surface OPENS**,
|
|
6
|
+
* not from one that stays mounted while it is closed. Every Bloom surface is
|
|
7
|
+
* already shaped that way (`Dialog.web` returns null while closed;
|
|
8
|
+
* `BottomSheetBase` returns null until `rendered`), and `OverlayRoot` — which
|
|
9
|
+
* lives inside those guards — is where the call actually sits for most of them.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
acquireOverlayRank,
|
|
15
|
+
layerForRank,
|
|
16
|
+
registerOverlayRank,
|
|
17
|
+
releaseOverlayRank,
|
|
18
|
+
type OverlayLayer,
|
|
19
|
+
} from './stack';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reserve this surface's place in the overlay stack for as long as it is
|
|
23
|
+
* mounted. Returns the z-indices it should paint with.
|
|
24
|
+
*/
|
|
25
|
+
export function useOverlayLayer(): OverlayLayer {
|
|
26
|
+
// A state initializer, not a render-body call: it runs once per mount, so the
|
|
27
|
+
// rank is fixed before the first paint (an effect would leave one frame at
|
|
28
|
+
// the wrong depth) and it is not a position the React Compiler can memoize
|
|
29
|
+
// into a stale read.
|
|
30
|
+
const [rank] = useState(acquireOverlayRank);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
registerOverlayRank(rank);
|
|
34
|
+
return () => releaseOverlayRank(rank);
|
|
35
|
+
}, [rank]);
|
|
36
|
+
|
|
37
|
+
const [layer] = useState(() => layerForRank(rank));
|
|
38
|
+
return layer;
|
|
39
|
+
}
|
|
@@ -10,9 +10,8 @@ import React, {
|
|
|
10
10
|
import { Pressable, StyleSheet, View } from 'react-native';
|
|
11
11
|
|
|
12
12
|
import { useTheme } from '../theme/use-theme';
|
|
13
|
-
import { Backdrop } from '../overlay';
|
|
13
|
+
import { Backdrop, OverlayRoot } from '../overlay';
|
|
14
14
|
import { Portal } from '../portal/index.web';
|
|
15
|
-
import { createOverlayZIndex } from '../styles/z-index';
|
|
16
15
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
17
16
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
18
17
|
import { PopoverContext, usePopoverContext } from './context';
|
|
@@ -36,7 +35,6 @@ export type {
|
|
|
36
35
|
|
|
37
36
|
export { usePopoverContext };
|
|
38
37
|
|
|
39
|
-
const popoverZIndex = createOverlayZIndex();
|
|
40
38
|
|
|
41
39
|
/**
|
|
42
40
|
* Web self-contained control. Unlike native (which proxies a `BottomSheet`
|
|
@@ -262,45 +260,52 @@ export function PopoverContent({
|
|
|
262
260
|
|
|
263
261
|
return (
|
|
264
262
|
<Portal>
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
263
|
+
{/* `OverlayRoot` takes this surface's place in the open-order overlay
|
|
264
|
+
stack, so it paints above anything opened before it (see
|
|
265
|
+
`src/overlay/stack.ts`). It is `box-none`, so the area outside the
|
|
266
|
+
panel stays click-through and the backdrop below still takes its own
|
|
267
|
+
presses. */}
|
|
268
|
+
<OverlayRoot>
|
|
269
|
+
{dismissible ? (
|
|
270
|
+
<Backdrop
|
|
271
|
+
style={styles.backdrop}
|
|
272
|
+
onPress={() => control.close()}
|
|
273
|
+
accessibilityLabel="Dismiss popover"
|
|
274
|
+
/>
|
|
275
|
+
) : null}
|
|
276
|
+
<View
|
|
277
|
+
ref={panelRef}
|
|
278
|
+
accessibilityRole="none"
|
|
279
|
+
aria-label={label}
|
|
280
|
+
testID={testID}
|
|
281
|
+
onLayout={(e) =>
|
|
282
|
+
setPanelSize({
|
|
283
|
+
width: e.nativeEvent.layout.width,
|
|
284
|
+
height: e.nativeEvent.layout.height,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
style={[
|
|
288
|
+
styles.panel,
|
|
289
|
+
{
|
|
290
|
+
left: position.left,
|
|
291
|
+
top: position.top,
|
|
292
|
+
minWidth: resolvedMinWidth,
|
|
293
|
+
maxWidth,
|
|
294
|
+
backgroundColor: theme.isDark
|
|
295
|
+
? theme.colors.backgroundSecondary
|
|
296
|
+
: theme.colors.background,
|
|
297
|
+
borderColor: theme.colors.borderLight,
|
|
298
|
+
// Design-system overlay elevation (`shadow-m`) as a `boxShadow` —
|
|
299
|
+
// RN-Web deprecated the `shadow*` style props.
|
|
300
|
+
...bloomShadowStyle('m'),
|
|
301
|
+
// Hide until measured to avoid a one-frame jump.
|
|
302
|
+
opacity: panelSize ? 1 : 0,
|
|
303
|
+
},
|
|
304
|
+
style,
|
|
305
|
+
]}>
|
|
306
|
+
{children}
|
|
307
|
+
</View>
|
|
308
|
+
</OverlayRoot>
|
|
304
309
|
</Portal>
|
|
305
310
|
);
|
|
306
311
|
}
|
|
@@ -315,12 +320,10 @@ const styles = StyleSheet.create({
|
|
|
315
320
|
left: 0,
|
|
316
321
|
right: 0,
|
|
317
322
|
bottom: 0,
|
|
318
|
-
zIndex: popoverZIndex.backdrop,
|
|
319
323
|
pointerEvents: 'auto',
|
|
320
324
|
},
|
|
321
325
|
panel: {
|
|
322
326
|
position: WEB_POSITION_FIXED,
|
|
323
|
-
zIndex: popoverZIndex.surface,
|
|
324
327
|
borderRadius: 12,
|
|
325
328
|
borderWidth: 1,
|
|
326
329
|
paddingVertical: 4,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
|
|
12
12
|
import { useTheme } from '../theme/use-theme';
|
|
13
|
+
import { OverlayRoot } from '../overlay';
|
|
13
14
|
import { Portal } from '../portal';
|
|
14
15
|
import { Z_INDEX } from '../styles/z-index';
|
|
15
16
|
import { PromptInputContext, type Attachment } from './context';
|
|
@@ -230,36 +231,42 @@ export function PromptInput({
|
|
|
230
231
|
|
|
231
232
|
{showFullscreen && (
|
|
232
233
|
<Portal>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
style={{
|
|
248
|
-
position: 'absolute',
|
|
249
|
-
top: 16,
|
|
250
|
-
right: 16,
|
|
251
|
-
zIndex: Z_INDEX.fullscreenControl,
|
|
252
|
-
padding: 8,
|
|
253
|
-
backgroundColor: theme.colors.background,
|
|
254
|
-
borderRadius: 9999,
|
|
255
|
-
}}
|
|
234
|
+
{/* `OverlayRoot` takes the fullscreen editor's place in the
|
|
235
|
+
open-order overlay stack (`src/overlay/stack.ts`). It mounts with
|
|
236
|
+
`showFullscreen`, so the rank tracks opening. */}
|
|
237
|
+
<OverlayRoot>
|
|
238
|
+
<View
|
|
239
|
+
style={[
|
|
240
|
+
StyleSheet.absoluteFill,
|
|
241
|
+
{
|
|
242
|
+
backgroundColor: theme.colors.background,
|
|
243
|
+
// Opt back in from the Portal root's `pointer-events: none`
|
|
244
|
+
// (web only — harmless on native).
|
|
245
|
+
pointerEvents: 'auto',
|
|
246
|
+
},
|
|
247
|
+
]}
|
|
256
248
|
>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
<Pressable
|
|
250
|
+
onPress={() => setShowFullscreen(false)}
|
|
251
|
+
style={{
|
|
252
|
+
position: 'absolute',
|
|
253
|
+
top: 16,
|
|
254
|
+
right: 16,
|
|
255
|
+
// Above the editor's own body; where the editor sits relative
|
|
256
|
+
// to OTHER surfaces is the overlay stack's call.
|
|
257
|
+
zIndex: Z_INDEX.raised,
|
|
258
|
+
padding: 8,
|
|
259
|
+
backgroundColor: theme.colors.background,
|
|
260
|
+
borderRadius: 9999,
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{collapseIcon ?? (
|
|
264
|
+
<Text style={{ fontSize: 18, color: theme.colors.text }}>⤡</Text>
|
|
265
|
+
)}
|
|
266
|
+
</Pressable>
|
|
267
|
+
<View style={{ flex: 1 }}>{content}</View>
|
|
268
|
+
</View>
|
|
269
|
+
</OverlayRoot>
|
|
263
270
|
</Portal>
|
|
264
271
|
)}
|
|
265
272
|
</PromptInputContext.Provider>
|
package/src/select/index.web.tsx
CHANGED
|
@@ -12,9 +12,8 @@ import { Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
|
|
|
12
12
|
|
|
13
13
|
import { useTheme } from '../theme/use-theme';
|
|
14
14
|
import { Text } from '../typography';
|
|
15
|
-
import { Backdrop } from '../overlay';
|
|
15
|
+
import { Backdrop, OverlayRoot } from '../overlay';
|
|
16
16
|
import { Portal } from '../portal/index.web';
|
|
17
|
-
import { createOverlayZIndex } from '../styles/z-index';
|
|
18
17
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
19
18
|
import { resolveDropdownPlacement } from '../overlay/dropdown-placement';
|
|
20
19
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
@@ -39,7 +38,6 @@ import type {
|
|
|
39
38
|
|
|
40
39
|
export { useSelectItemContext };
|
|
41
40
|
|
|
42
|
-
const selectZIndex = createOverlayZIndex();
|
|
43
41
|
const VIEWPORT_GUTTER = 8;
|
|
44
42
|
const SELECT_OFFSET = 6;
|
|
45
43
|
|
|
@@ -259,33 +257,40 @@ export function SelectContent<T>({
|
|
|
259
257
|
|
|
260
258
|
return (
|
|
261
259
|
<Portal>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
260
|
+
{/* `OverlayRoot` takes this surface's place in the open-order overlay
|
|
261
|
+
stack, so it paints above anything opened before it (see
|
|
262
|
+
`src/overlay/stack.ts`). It is `box-none`, so the area outside the
|
|
263
|
+
panel stays click-through and the backdrop below still takes its own
|
|
264
|
+
presses. */}
|
|
265
|
+
<OverlayRoot>
|
|
266
|
+
<Backdrop
|
|
267
|
+
style={styles.backdrop}
|
|
268
|
+
onPress={ctx.close}
|
|
269
|
+
accessibilityLabel="Close selection"
|
|
270
|
+
/>
|
|
271
|
+
<View
|
|
272
|
+
ref={attachDropdown}
|
|
273
|
+
accessibilityRole="list"
|
|
274
|
+
accessibilityLabel={label}
|
|
275
|
+
style={[
|
|
276
|
+
styles.dropdown,
|
|
277
|
+
{
|
|
278
|
+
backgroundColor: theme.isDark
|
|
279
|
+
? theme.colors.backgroundSecondary
|
|
280
|
+
: theme.colors.background,
|
|
281
|
+
borderColor: theme.colors.borderLight,
|
|
282
|
+
...bloomShadowStyle('m'),
|
|
283
|
+
},
|
|
284
|
+
position,
|
|
285
|
+
]}
|
|
286
|
+
>
|
|
287
|
+
{items.map((item, index) => (
|
|
288
|
+
<React.Fragment key={valueExtractor(item)}>
|
|
289
|
+
{renderItem(item, index, ctx.value)}
|
|
290
|
+
</React.Fragment>
|
|
291
|
+
))}
|
|
292
|
+
</View>
|
|
293
|
+
</OverlayRoot>
|
|
289
294
|
</Portal>
|
|
290
295
|
);
|
|
291
296
|
}
|
|
@@ -415,7 +420,6 @@ const styles = StyleSheet.create({
|
|
|
415
420
|
left: 0,
|
|
416
421
|
right: 0,
|
|
417
422
|
bottom: 0,
|
|
418
|
-
zIndex: selectZIndex.backdrop,
|
|
419
423
|
// Opt back in from the Portal root's `pointer-events: none`.
|
|
420
424
|
pointerEvents: 'auto',
|
|
421
425
|
},
|
|
@@ -428,7 +432,6 @@ const styles = StyleSheet.create({
|
|
|
428
432
|
position: WEB_POSITION_FIXED,
|
|
429
433
|
top: 0,
|
|
430
434
|
left: 0,
|
|
431
|
-
zIndex: selectZIndex.surface,
|
|
432
435
|
borderRadius: 8,
|
|
433
436
|
borderWidth: 1,
|
|
434
437
|
overflow: 'hidden',
|
package/src/styles/z-index.ts
CHANGED
|
@@ -1,47 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stacking scale for stacking WITHIN a single context.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* ## What belongs here, and what does not
|
|
5
|
+
*
|
|
6
|
+
* These values order elements against their own siblings — a badge over its
|
|
7
|
+
* avatar, a floating control over the panel it sits on, a sheet's drag handle
|
|
8
|
+
* over the sheet body. That is a local decision with a local answer, so a
|
|
9
|
+
* constant is the right tool.
|
|
10
|
+
*
|
|
11
|
+
* **Which OVERLAY SURFACE is on top of which is NOT on this scale**, and must
|
|
12
|
+
* never be added back to it. That question is answered by open order — the
|
|
13
|
+
* surface the user opened last wins — and is owned by `src/overlay/stack.ts`.
|
|
14
|
+
*
|
|
15
|
+
* This file used to carry rungs for that too (`dropdown` 40/41, `overlay`
|
|
16
|
+
* 50/60, `tooltip` 70, `toast` 80, `fullscreen` 90/91), plus
|
|
17
|
+
* `createOverlayZIndex` / `createDropdownZIndex` / `Z_INDEX_LAYER_STEP` to
|
|
18
|
+
* offset them. Because a rung is fixed per COMPONENT KIND, it decided stacking
|
|
19
|
+
* by what a surface WAS rather than by when it opened, so some pairings were
|
|
20
|
+
* permanently inverted: a confirm dialog (50/60) opened from inside an open
|
|
21
|
+
* bottom sheet (999999) rendered underneath it and could not be pressed or
|
|
22
|
+
* dismissed, and a menu (40/41) opened from inside a dialog landed behind that
|
|
23
|
+
* dialog. Those rungs and helpers are gone; reintroducing one reintroduces the
|
|
24
|
+
* bug.
|
|
7
25
|
*/
|
|
8
26
|
export const Z_INDEX = {
|
|
27
|
+
/** Default resting level. */
|
|
9
28
|
base: 0,
|
|
29
|
+
/** One step above an immediate sibling — a badge over its avatar. */
|
|
10
30
|
raised: 1,
|
|
31
|
+
/** A control floating over the content it belongs to. */
|
|
11
32
|
floating: 10,
|
|
12
|
-
|
|
33
|
+
/** A dropdown's INLINE trigger wrapper, still in the app's document flow. */
|
|
13
34
|
dropdown: 40,
|
|
14
|
-
|
|
15
|
-
overlayBackdrop: 50,
|
|
16
|
-
overlaySurface: 60,
|
|
17
|
-
tooltip: 70,
|
|
18
|
-
toast: 80,
|
|
19
|
-
fullscreen: 90,
|
|
20
|
-
fullscreenControl: 91,
|
|
35
|
+
/** A bottom sheet's drag handle, over the sheet's own body. */
|
|
21
36
|
sheetHandle: 100,
|
|
37
|
+
/**
|
|
38
|
+
* The document-level portal container itself (`#bloom-portal-root`). Overlay
|
|
39
|
+
* surfaces render INSIDE it and order among themselves by open order — none
|
|
40
|
+
* of them may claim this value, which is what made the sheet unbeatable.
|
|
41
|
+
*/
|
|
22
42
|
portalRoot: 999999,
|
|
23
43
|
} as const;
|
|
24
44
|
|
|
25
|
-
export const Z_INDEX_LAYER_STEP = 10;
|
|
26
|
-
|
|
27
|
-
function layerOffset(layer = 0) {
|
|
28
|
-
return Math.max(0, Math.floor(layer)) * Z_INDEX_LAYER_STEP;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createDropdownZIndex(layer = 0) {
|
|
32
|
-
const offset = layerOffset(layer);
|
|
33
|
-
return {
|
|
34
|
-
root: Z_INDEX.dropdown + offset,
|
|
35
|
-
surface: Z_INDEX.dropdownSurface + offset,
|
|
36
|
-
} as const;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createOverlayZIndex(layer = 0) {
|
|
40
|
-
const offset = layerOffset(layer);
|
|
41
|
-
return {
|
|
42
|
-
backdrop: Z_INDEX.overlayBackdrop + offset,
|
|
43
|
-
surface: Z_INDEX.overlaySurface + offset,
|
|
44
|
-
} as const;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
45
|
export const zIndex = Z_INDEX;
|
|
@@ -67,13 +67,7 @@ export function createSurfaceHost(Dialog: DialogComponent) {
|
|
|
67
67
|
* dismiss REQUEST and has no post-exit callback, so it could never tell the
|
|
68
68
|
* store WHEN to splice the entry after the exit animation.
|
|
69
69
|
*/
|
|
70
|
-
function SurfaceLayer({
|
|
71
|
-
entry,
|
|
72
|
-
index,
|
|
73
|
-
}: {
|
|
74
|
-
entry: SurfaceEntry;
|
|
75
|
-
index: number;
|
|
76
|
-
}) {
|
|
70
|
+
function SurfaceLayer({ entry }: { entry: SurfaceEntry }) {
|
|
77
71
|
const control = useDialogControl();
|
|
78
72
|
const { id, status } = entry;
|
|
79
73
|
|
|
@@ -125,7 +119,6 @@ export function createSurfaceHost(Dialog: DialogComponent) {
|
|
|
125
119
|
<Dialog
|
|
126
120
|
control={control}
|
|
127
121
|
onClose={handleClose}
|
|
128
|
-
layer={index}
|
|
129
122
|
startOpen={status !== 'closing'}
|
|
130
123
|
{...placement}
|
|
131
124
|
>
|
|
@@ -144,8 +137,8 @@ export function createSurfaceHost(Dialog: DialogComponent) {
|
|
|
144
137
|
|
|
145
138
|
return (
|
|
146
139
|
<>
|
|
147
|
-
{stack.map((entry
|
|
148
|
-
<SurfaceLayer key={entry.id} entry={entry}
|
|
140
|
+
{stack.map((entry) => (
|
|
141
|
+
<SurfaceLayer key={entry.id} entry={entry} />
|
|
149
142
|
))}
|
|
150
143
|
</>
|
|
151
144
|
);
|
package/src/toast/ToastHost.tsx
CHANGED
|
@@ -26,10 +26,9 @@ import * as React from 'react';
|
|
|
26
26
|
import { StyleSheet } from 'react-native';
|
|
27
27
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
28
28
|
|
|
29
|
-
import { OverlayRoot } from '../overlay';
|
|
29
|
+
import { OverlayRoot, TOAST_LAYER_Z } from '../overlay';
|
|
30
30
|
import { Portal } from '../portal/index.web';
|
|
31
31
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
32
|
-
import { Z_INDEX } from '../styles/z-index';
|
|
33
32
|
import type { ToastHostProps } from './types';
|
|
34
33
|
|
|
35
34
|
export function ToastHost({ children, ToasterOverlayWrapper }: ToastHostProps) {
|
|
@@ -37,7 +36,7 @@ export function ToastHost({ children, ToasterOverlayWrapper }: ToastHostProps) {
|
|
|
37
36
|
// `OverlayRoot` carries the pointer-events opt-in as a PROP; as a style
|
|
38
37
|
// entry it never reached the DOM, so the rows inherited the portal root's
|
|
39
38
|
// `pointer-events: none` and no toast could be pressed or swiped away.
|
|
40
|
-
<OverlayRoot style={styles.host}>
|
|
39
|
+
<OverlayRoot zIndex={TOAST_LAYER_Z} style={styles.host}>
|
|
41
40
|
{/* Also `box-none`: as the direct child of a box-none root it would
|
|
42
41
|
otherwise be handed `pointer-events: auto` and, spanning the whole
|
|
43
42
|
viewport, block the app underneath while a toast is up. */}
|
|
@@ -68,8 +67,12 @@ const styles = StyleSheet.create({
|
|
|
68
67
|
left: 0,
|
|
69
68
|
right: 0,
|
|
70
69
|
bottom: 0,
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
70
|
+
// The depth is `TOAST_LAYER_Z`, applied by `OverlayRoot` via its `zIndex`
|
|
71
|
+
// prop — a toast is a notification, not a modal surface, so it is pinned
|
|
72
|
+
// above the whole open-order overlay stack rather than taking a rank in it
|
|
73
|
+
// (see `src/overlay/stack.ts`). It must not be set here as well: a style
|
|
74
|
+
// z-index would fight the prop and, being a fixed number again, would land
|
|
75
|
+
// back under any surface that outranked it — which is how toasts were
|
|
76
|
+
// already being lost behind an open bottom sheet.
|
|
74
77
|
},
|
|
75
78
|
});
|