@celestia-island/hikari 0.40.25 → 0.40.26
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/package.json
CHANGED
|
@@ -198,3 +198,83 @@ describe("usePopupManager blocking flag (breadcrumb levels)", () => {
|
|
|
198
198
|
}
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
describe("usePopupManager window stack (blocking sheets reband)", () => {
|
|
203
|
+
it("stacks a blocking sheet with windows in open order — a window opened from inside the sheet paints above it", () => {
|
|
204
|
+
// The chest mobile regression: the theme menu docks as a bottom
|
|
205
|
+
// sheet, its row's edit affordance opens a MODAL — kind bands put
|
|
206
|
+
// the sheet (dropdown, 2000) above the modal (1000) and the editor
|
|
207
|
+
// rendered BEHIND its own opener. A blocking sheet is a window: it
|
|
208
|
+
// must share the window band and lose to windows pushed after it.
|
|
209
|
+
const m = freshManager();
|
|
210
|
+
const sheet = m.register("dropdown", false, "Themes", true);
|
|
211
|
+
expect(sheet.zIndex).toBe(POPUP_Z_BANDS.modal);
|
|
212
|
+
|
|
213
|
+
const editor = m.register("modal", true, "Edit theme");
|
|
214
|
+
expect(editor.zIndex).toBe(sheet.zIndex + POPUP_Z_STEP);
|
|
215
|
+
|
|
216
|
+
// A sheet opened from INSIDE that modal pushes on top of it.
|
|
217
|
+
const innerSheet = m.register("dropdown", false, "Picker", true);
|
|
218
|
+
expect(innerSheet.zIndex).toBe(editor.zIndex + POPUP_Z_STEP);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("keeps an ANCHORED dropdown above the whole window stack, blocking sheets included", () => {
|
|
222
|
+
// The in-modal select flow: anchored panels stay in the dropdown
|
|
223
|
+
// band so they paint above whichever window (modal OR blocking
|
|
224
|
+
// sheet) contains them.
|
|
225
|
+
const m = freshManager();
|
|
226
|
+
const sheet = m.register("dropdown", false, "Themes", true);
|
|
227
|
+
const modal = m.register("modal", true, "Edit");
|
|
228
|
+
const panel = m.register("dropdown", false);
|
|
229
|
+
expect(panel.zIndex).toBe(POPUP_Z_BANDS.dropdown);
|
|
230
|
+
expect(panel.zIndex).toBeGreaterThan(sheet.zIndex);
|
|
231
|
+
expect(panel.zIndex).toBeGreaterThan(modal.zIndex);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("reclaims window-band slots across mixed windows and sheets", () => {
|
|
235
|
+
const m = freshManager();
|
|
236
|
+
const sheet = m.register("dropdown", false, "Themes", true);
|
|
237
|
+
const modal = m.register("modal", true, "Edit");
|
|
238
|
+
expect(modal.zIndex).toBe(sheet.zIndex + POPUP_Z_STEP);
|
|
239
|
+
|
|
240
|
+
m.unregister(modal.id);
|
|
241
|
+
const drawer = m.register("drawer", true, "Details");
|
|
242
|
+
// The drawer reclaims the modal's slot, not a third one.
|
|
243
|
+
expect(drawer.zIndex).toBe(modal.zIndex);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("setBlocking promotion pushes onto the top of the window band, demotion returns to the anchored band", () => {
|
|
247
|
+
const m = freshManager();
|
|
248
|
+
const modal = m.register("modal", true, "Edit");
|
|
249
|
+
// Anchored first: above every window while non-blocking.
|
|
250
|
+
const popup = m.register("dropdown", false, "Menu");
|
|
251
|
+
expect(popup.zIndex).toBe(POPUP_Z_BANDS.dropdown);
|
|
252
|
+
|
|
253
|
+
// Viewport crosses the mobile breakpoint: the popover docks as a
|
|
254
|
+
// sheet — becoming a window is a PUSH, so it lands above the modal.
|
|
255
|
+
m.setBlocking(popup.id, true);
|
|
256
|
+
expect(m.registry.value.get(popup.id)!.zIndex).toBe(modal.zIndex + POPUP_Z_STEP);
|
|
257
|
+
|
|
258
|
+
// Back to desktop: an anchored attachment again — top of the
|
|
259
|
+
// anchored band, above the whole window stack.
|
|
260
|
+
m.setBlocking(popup.id, false);
|
|
261
|
+
const entry = m.registry.value.get(popup.id)!;
|
|
262
|
+
expect(entry.zIndex).toBe(POPUP_Z_BANDS.dropdown);
|
|
263
|
+
expect(entry.zIndex).toBeGreaterThan(modal.zIndex);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("orders the breadcrumb window stack by push order (sheet → editor)", () => {
|
|
267
|
+
// HkModalBreadcrumb sorts by zIndex; with blocking sheets sharing
|
|
268
|
+
// the window band, the strip reads in navigation order: the sheet
|
|
269
|
+
// that opened the editor, then the editor as the current level.
|
|
270
|
+
const m = freshManager();
|
|
271
|
+
const sheet = m.register("dropdown", false, "Themes", true);
|
|
272
|
+
const editor = m.register("modal", true, "Edit theme");
|
|
273
|
+
const levels = [...m.registry.value.values()]
|
|
274
|
+
.filter((e) => e.kind === "modal" || e.kind === "drawer" || e.blocking)
|
|
275
|
+
.sort((a, b) => a.zIndex - b.zIndex)
|
|
276
|
+
.map((e) => e.title);
|
|
277
|
+
expect(levels).toEqual(["Themes", "Edit theme"]);
|
|
278
|
+
expect(editor.zIndex).toBeGreaterThan(sheet.zIndex);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -5,23 +5,28 @@ export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
|
|
|
5
5
|
/**
|
|
6
6
|
* Kind-priority z bands — the single source of truth for popup stacking.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* by
|
|
10
|
-
* ordered low → high:
|
|
8
|
+
* Layering is decided by WHAT a surface IS — window or attachment — never
|
|
9
|
+
* by WHEN it happened to register:
|
|
11
10
|
*
|
|
12
|
-
* modal (1000) centered dialogs + phone bottom
|
|
13
|
-
* drawer (1000) edge drawers
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
11
|
+
* modal (1000) the WINDOW band: centered dialogs + phone bottom
|
|
12
|
+
* drawer (1000) sheets + edge drawers. Anything that BLOCKS the page
|
|
13
|
+
* like a window is a window: modals and drawers by
|
|
14
|
+
* kind, and a dropdown-kind surface while it is docked
|
|
15
|
+
* as a mobile bottom sheet (`blocking`). Within the
|
|
16
|
+
* band surfaces stack purely in open order (push/pop
|
|
17
|
+
* stack semantics), so a window opened FROM inside a
|
|
18
|
+
* sheet always paints above it — the sheet keeps no
|
|
19
|
+
* kind-priority claim over windows opened later.
|
|
20
|
+
* dropdown (2000) ANCHORED popovers, select panels, menus — surfaces
|
|
21
|
+
* attached to an anchor on whatever window is on top,
|
|
22
|
+
* not windows themselves. ABOVE the window band on
|
|
23
|
+
* purpose: a select panel opened from inside a modal
|
|
24
|
+
* portals to <body> and must paint above the modal
|
|
25
|
+
* that contains it (the common in-modal form flow). A
|
|
26
|
+
* page-level anchored dropdown can only coexist with a
|
|
27
|
+
* modal programmatically (the modal overlay intercepts
|
|
28
|
+
* pointers), so the rare stale-dropdown-over-modal case
|
|
29
|
+
* is accepted, matching Ant Design
|
|
25
30
|
* tooltip (3000) transient annotations must stay visible above the
|
|
26
31
|
* surfaces they annotate
|
|
27
32
|
* toast (4000) ALWAYS the topmost surface — a toast must never be
|
|
@@ -33,8 +38,11 @@ export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
|
|
|
33
38
|
* freed slots are reclaimed automatically because the next z derives from
|
|
34
39
|
* the CURRENT live entries of that band only — no monotonic counter, no
|
|
35
40
|
* cross-band coupling (a persistent toast/tooltip registration can no
|
|
36
|
-
* longer push later modals up the ladder
|
|
37
|
-
*
|
|
41
|
+
* longer push later modals up the ladder). A dropdown flipping its
|
|
42
|
+
* blocking flag (anchored popover docking as a sheet, or the reverse)
|
|
43
|
+
* REBANDS in place: promotion pushes onto the top of the window band
|
|
44
|
+
* (becoming a window IS a push), demotion lands on the top of the
|
|
45
|
+
* anchored band. Overlay roots are spaced one Z_STEP apart so each
|
|
38
46
|
* overlay's +1 content/panel layer always has a free slot above its own
|
|
39
47
|
* root and below the next overlay.
|
|
40
48
|
*
|
|
@@ -49,6 +57,17 @@ export const POPUP_Z_BANDS: Record<PopupKind, number> = {
|
|
|
49
57
|
toast: 4000,
|
|
50
58
|
};
|
|
51
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The band a popup CURRENTLY stacks in. Dropdown-kind surfaces have two:
|
|
62
|
+
* the anchored band while attached to an anchor, the WINDOW band
|
|
63
|
+
* (shared with modals/drawers) while docked as a blocking bottom sheet.
|
|
64
|
+
* Window kinds always live in the window band.
|
|
65
|
+
*/
|
|
66
|
+
function effectiveBand(kind: PopupKind, blocking: boolean): number {
|
|
67
|
+
if (kind === "dropdown") return blocking ? POPUP_Z_BANDS.modal : POPUP_Z_BANDS.dropdown;
|
|
68
|
+
return POPUP_Z_BANDS[kind];
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/** In-band stacking step. Even numbers leave the odd slot free for the
|
|
53
72
|
* +1 content/panel layer each overlay puts above its own root. */
|
|
54
73
|
export const POPUP_Z_STEP = 2;
|
|
@@ -61,11 +80,15 @@ export interface PopupEntry {
|
|
|
61
80
|
title?: string;
|
|
62
81
|
/**
|
|
63
82
|
* True while the popup is a blocking window the user "navigates" —
|
|
64
|
-
* a mobile bottom sheet that rose from a dropdown-kind surface.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
83
|
+
* a mobile bottom sheet that rose from a dropdown-kind surface. This
|
|
84
|
+
* flag is window-stack MEMBERSHIP: it lists the popup in the
|
|
85
|
+
* modal-stack breadcrumb AND moves it into the window z band (see
|
|
86
|
+
* effectiveBand) — a blocking sheet is a window in every sense, so
|
|
87
|
+
* windows opened from inside it stack above it. Window kinds
|
|
88
|
+
* (modal/drawer) are windows by kind and always listed; an anchored
|
|
89
|
+
* desktop popover stays a hidden level. Named surfaces only: a
|
|
90
|
+
* blocking popup without a title is a naming bug (dev warn at
|
|
91
|
+
* registration).
|
|
69
92
|
*/
|
|
70
93
|
blocking: boolean;
|
|
71
94
|
}
|
|
@@ -121,11 +144,14 @@ export function usePopupManager() {
|
|
|
121
144
|
blocking = false,
|
|
122
145
|
): PopupHandle {
|
|
123
146
|
const id = uid();
|
|
124
|
-
const band =
|
|
147
|
+
const band = effectiveBand(kind, blocking);
|
|
125
148
|
// Next slot = one step above the highest LIVE entry of the same band.
|
|
149
|
+
// Band membership follows the entry's CURRENT shape (see
|
|
150
|
+
// effectiveBand): a blocking sheet scans the window band, so it lands
|
|
151
|
+
// above every window opened before it.
|
|
126
152
|
let maxSlot = -1;
|
|
127
153
|
for (const entry of registry.value.values()) {
|
|
128
|
-
if (
|
|
154
|
+
if (effectiveBand(entry.kind, entry.blocking) !== band) continue;
|
|
129
155
|
const slot = (entry.zIndex - band) / POPUP_Z_STEP;
|
|
130
156
|
if (slot > maxSlot) maxSlot = slot;
|
|
131
157
|
}
|
|
@@ -151,11 +177,26 @@ export function usePopupManager() {
|
|
|
151
177
|
* Flip the blocking flag while the popup stays open — a dropdown that
|
|
152
178
|
* docks as a bottom sheet when the viewport crosses the mobile
|
|
153
179
|
* breakpoint becomes a breadcrumb level mid-flight (and back).
|
|
180
|
+
*
|
|
181
|
+
* The flip also REBANDS the z: promotion pushes the sheet onto the top
|
|
182
|
+
* of the window band (becoming a window is a push — it must cover the
|
|
183
|
+
* page), demotion lands it on the top of the anchored band. The freed
|
|
184
|
+
* slot in the old band reclaims lazily via the usual max-live-slot
|
|
185
|
+
* derivation.
|
|
154
186
|
*/
|
|
155
187
|
function setBlocking(id: string, blocking: boolean) {
|
|
156
188
|
const entry = registry.value.get(id);
|
|
157
189
|
if (!entry || entry.blocking === blocking) return;
|
|
158
190
|
entry.blocking = blocking;
|
|
191
|
+
const band = effectiveBand(entry.kind, entry.blocking);
|
|
192
|
+
let maxSlot = -1;
|
|
193
|
+
for (const other of registry.value.values()) {
|
|
194
|
+
if (other === entry) continue;
|
|
195
|
+
if (effectiveBand(other.kind, other.blocking) !== band) continue;
|
|
196
|
+
const slot = (other.zIndex - band) / POPUP_Z_STEP;
|
|
197
|
+
if (slot > maxSlot) maxSlot = slot;
|
|
198
|
+
}
|
|
199
|
+
entry.zIndex = band + (maxSlot + 1) * POPUP_Z_STEP;
|
|
159
200
|
registry.value = new Map(registry.value);
|
|
160
201
|
if (blocking) warnUntitled(entry.kind, true, entry.title);
|
|
161
202
|
}
|