@lotics/ui 11.7.2 → 11.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/catalog.md +6 -4
- package/package.json +1 -1
- package/src/combobox.tsx +2 -2
- package/src/file_gallery_modal.tsx +2 -1
- package/src/option_list.tsx +2 -2
- package/src/popover.tsx +41 -11
- package/src/popover_layers.test.ts +113 -0
- package/src/popover_layers.ts +57 -0
package/docs/catalog.md
CHANGED
|
@@ -676,10 +676,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
676
676
|
own scroll, like `OptionList`): the anchored floating surface. **NON-MODAL**: the anchored
|
|
677
677
|
popover has NO blocking overlay — the rest of the page stays interactive, and clicking another
|
|
678
678
|
control both dismisses this popover AND activates that control in one click; clicking outside,
|
|
679
|
-
scrolling an ancestor, or Escape dismisses.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
`
|
|
679
|
+
scrolling an ancestor, or Escape dismisses. Interactions inside a layer the popover opened
|
|
680
|
+
from within (a nested popover, an `Alert` confirm, a full-screen Modal like the file preview)
|
|
681
|
+
never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes. Only the
|
|
682
|
+
`small` (bottom-sheet) presentation is modal (scrim). `PopoverContent` already insets its
|
|
683
|
+
body 12px — put content directly in it, NEVER add your own padding `View` (that double-pads);
|
|
684
|
+
title/actions go in `PopoverHeader` / `PopoverFooter`.
|
|
683
685
|
- **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
|
|
684
686
|
built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes,
|
|
685
687
|
`goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the
|
package/package.json
CHANGED
package/src/combobox.tsx
CHANGED
|
@@ -511,7 +511,6 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
511
511
|
const label = opt.label ?? opt.value;
|
|
512
512
|
const content = !isCustom ? renderOptionContent?.(opt) : undefined;
|
|
513
513
|
const shared = {
|
|
514
|
-
key: `${row.kind}-${opt.value}`,
|
|
515
514
|
nativeID: row.nativeID,
|
|
516
515
|
testID: isCustom ? "combobox-custom-option" : `combobox-option-${opt.value}`,
|
|
517
516
|
role: "option" as const,
|
|
@@ -526,9 +525,10 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
526
525
|
// Rich row (custom content, or a label over its description) → the growing
|
|
527
526
|
// `MenuListItem`; a plain option stays the fixed-height `MenuButton`.
|
|
528
527
|
return content || desc ? (
|
|
529
|
-
<MenuListItem {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
528
|
+
<MenuListItem key={`${row.kind}-${opt.value}`} {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
530
529
|
) : (
|
|
531
530
|
<MenuButton
|
|
531
|
+
key={`${row.kind}-${opt.value}`}
|
|
532
532
|
{...shared}
|
|
533
533
|
title={
|
|
534
534
|
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
|
@@ -115,6 +115,7 @@ export function FileGalleryModal(props: FileGalleryModalProps) {
|
|
|
115
115
|
// additionally can't hold filename + every action inline (the close button gets
|
|
116
116
|
// pushed off-screen), so the remaining actions collapse into a ⋯ menu.
|
|
117
117
|
const { small } = useScreenSize();
|
|
118
|
+
const locale = useLoticsLocale();
|
|
118
119
|
|
|
119
120
|
const close = useCallback(() => onIndexChange(null), [onIndexChange]);
|
|
120
121
|
const next = useCallback(() => {
|
|
@@ -154,7 +155,7 @@ export function FileGalleryModal(props: FileGalleryModalProps) {
|
|
|
154
155
|
const file = files[activeIndex];
|
|
155
156
|
if (!file) return null;
|
|
156
157
|
|
|
157
|
-
const l = { ...
|
|
158
|
+
const l = { ...locale.gallery, ...labels };
|
|
158
159
|
const isImage = isImageMimeType(file.mimeType);
|
|
159
160
|
const total = files.length;
|
|
160
161
|
const rotated = rot.rotationFor(file.id) !== 0;
|
package/src/option_list.tsx
CHANGED
|
@@ -122,7 +122,6 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
122
122
|
const label = opt.label ?? opt.value;
|
|
123
123
|
const content = row.kind === "option" ? renderOptionContent?.(opt) : undefined;
|
|
124
124
|
const shared = {
|
|
125
|
-
key: `${row.kind}-${opt.value}`,
|
|
126
125
|
nativeID: row.nativeID,
|
|
127
126
|
testID: isCustom ? "option-list-custom" : opt.testID || `picker-option-${opt.value}`,
|
|
128
127
|
role: "option" as const,
|
|
@@ -146,9 +145,10 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
146
145
|
// description — is `MenuListItem`: the listbox row that GROWS. `MenuButton` is
|
|
147
146
|
// the fixed-height single-line menu row, and stays that for plain options.
|
|
148
147
|
return content || desc ? (
|
|
149
|
-
<MenuListItem {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
148
|
+
<MenuListItem key={`${row.kind}-${opt.value}`} {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
150
149
|
) : (
|
|
151
150
|
<MenuButton
|
|
151
|
+
key={`${row.kind}-${opt.value}`}
|
|
152
152
|
{...shared}
|
|
153
153
|
title={
|
|
154
154
|
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
package/src/popover.tsx
CHANGED
|
@@ -13,6 +13,11 @@ import { IconButton } from "./icon_button";
|
|
|
13
13
|
import { Portal } from "./portal";
|
|
14
14
|
import { Divider } from "./divider";
|
|
15
15
|
import { useOverlayScope } from "./overlay_scope";
|
|
16
|
+
import {
|
|
17
|
+
hasModalLayerAbove,
|
|
18
|
+
isInLayerAbove,
|
|
19
|
+
snapshotOpenModalLayers,
|
|
20
|
+
} from "./popover_layers";
|
|
16
21
|
import { PopoverNavContext, type PopoverNavContextValue } from "./popover_nav";
|
|
17
22
|
|
|
18
23
|
export type PopoverSide = "top" | "right" | "bottom" | "left";
|
|
@@ -216,6 +221,11 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
216
221
|
// hover/focus — so positioning cannot rely on `triggerRef.current` still being
|
|
217
222
|
// live when it runs.
|
|
218
223
|
const triggerRectRef = useRef<DOMRect | null>(null);
|
|
224
|
+
// Modal layers ([aria-modal]) open at the moment this popover opened. Any
|
|
225
|
+
// aria-modal element that appears LATER was opened from within this popover
|
|
226
|
+
// (a file-preview Modal, an Alert confirm) and stacks ABOVE it — interactions
|
|
227
|
+
// inside it must not dismiss this popover. See popover_layers.ts.
|
|
228
|
+
const modalsAtOpenRef = useRef<ReadonlySet<Element> | null>(null);
|
|
219
229
|
|
|
220
230
|
const handleClose = useCallback(() => {
|
|
221
231
|
if (!open) return;
|
|
@@ -232,6 +242,7 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
232
242
|
if (triggerRef.current) {
|
|
233
243
|
triggerRectRef.current = triggerRef.current.getBoundingClientRect();
|
|
234
244
|
}
|
|
245
|
+
modalsAtOpenRef.current = snapshotOpenModalLayers(document);
|
|
235
246
|
}, [open, small, triggerRef]);
|
|
236
247
|
|
|
237
248
|
// Focus management: when the popover opens, remember what had focus and move
|
|
@@ -301,6 +312,12 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
301
312
|
const handleEscape = (e: KeyboardEvent) => {
|
|
302
313
|
if (e.key !== "Escape") return;
|
|
303
314
|
|
|
315
|
+
// A modal opened from within (a file preview, an Alert) is stacked above
|
|
316
|
+
// this popover: RN-web's own document keyup listener closes the topmost
|
|
317
|
+
// modal. Stopping the event here would instead close this popover and
|
|
318
|
+
// unmount that modal with it — so yield while any such layer is open.
|
|
319
|
+
if (hasModalLayerAbove(document, modalsAtOpenRef.current)) return;
|
|
320
|
+
|
|
304
321
|
const allPopovers = document.querySelectorAll("[data-popover]");
|
|
305
322
|
const lastPopover = allPopovers[allPopovers.length - 1];
|
|
306
323
|
if (popoverRef.current !== lastPopover) return;
|
|
@@ -321,10 +338,11 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
321
338
|
// this popover's content AND its trigger. We listen on `click` (not pointerdown)
|
|
322
339
|
// in the CAPTURE phase so that clicking ANOTHER field's trigger completes that
|
|
323
340
|
// trigger's own press — the old popover dismisses AND the new one opens in a
|
|
324
|
-
// single click, instead of the press being cancelled mid-gesture. A click
|
|
325
|
-
// a
|
|
326
|
-
// popover
|
|
327
|
-
//
|
|
341
|
+
// single click, instead of the press being cancelled mid-gesture. A click inside
|
|
342
|
+
// a layer stacked ABOVE this popover keeps it open — a Select nested inside a
|
|
343
|
+
// filter popover, or a file-preview Modal / Alert opened from within — those are
|
|
344
|
+
// interactions WITH the popover's own flow, not a move away from it. The bottom
|
|
345
|
+
// sheet (`small`) is modal and dismisses via its scrim, so it opts out here.
|
|
328
346
|
useEffect(() => {
|
|
329
347
|
if (!open || small) return;
|
|
330
348
|
const onOutsideClick = (e: MouseEvent) => {
|
|
@@ -332,12 +350,8 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
332
350
|
if (!node) return;
|
|
333
351
|
if (popoverRef.current?.contains(node)) return; // inside my content
|
|
334
352
|
if (triggerRef.current?.contains(node)) return; // on my trigger — its own press toggles
|
|
335
|
-
// Keep open when the click is inside a DEEPER popover (e.g. a Select opened
|
|
336
|
-
// inside this one). `data-popover-level` is the same nesting depth as z-index.
|
|
337
353
|
const myLevel = Number(popoverRef.current?.getAttribute("data-popover-level") ?? "0");
|
|
338
|
-
|
|
339
|
-
const hit = el?.closest("[data-popover]");
|
|
340
|
-
if (hit && Number(hit.getAttribute("data-popover-level") ?? "0") > myLevel) return; // deeper popover
|
|
354
|
+
if (isInLayerAbove(node, myLevel, modalsAtOpenRef.current)) return;
|
|
341
355
|
onOpenChange(false);
|
|
342
356
|
};
|
|
343
357
|
document.addEventListener("click", onOutsideClick, true);
|
|
@@ -476,6 +490,7 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
476
490
|
setPosition((previous) => (previous === null ? previous : null));
|
|
477
491
|
setIsBottomSheetShown((previous) => (previous ? false : previous));
|
|
478
492
|
triggerRectRef.current = null;
|
|
493
|
+
modalsAtOpenRef.current = null;
|
|
479
494
|
return;
|
|
480
495
|
}
|
|
481
496
|
|
|
@@ -511,9 +526,14 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
511
526
|
// page or any ancestor is a "moved on" signal, so we DISMISS (as native
|
|
512
527
|
// <select> and most menu systems do) rather than chase the trigger and risk a
|
|
513
528
|
// panel detached from an off-screen anchor. Scrolling the popover's OWN content
|
|
514
|
-
// (a long option list) must NOT close it
|
|
529
|
+
// (a long option list) must NOT close it, and neither must a scroll inside a
|
|
530
|
+
// layer stacked above it (a nested popover's list, a file-preview Modal opened
|
|
531
|
+
// from within) — the anchor didn't move. Capture catches nested scrollers.
|
|
515
532
|
const onScroll = (e: Event) => {
|
|
516
|
-
|
|
533
|
+
const node = e.target instanceof Node ? e.target : null;
|
|
534
|
+
if (node && popoverRef.current?.contains(node)) return;
|
|
535
|
+
const myLevel = Number(popoverRef.current?.getAttribute("data-popover-level") ?? "0");
|
|
536
|
+
if (node && isInLayerAbove(node, myLevel, modalsAtOpenRef.current)) return;
|
|
517
537
|
onOpenChange(false);
|
|
518
538
|
};
|
|
519
539
|
window.addEventListener("scroll", onScroll, true);
|
|
@@ -626,6 +646,16 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
626
646
|
}),
|
|
627
647
|
}}
|
|
628
648
|
onClick={(e) => e.stopPropagation()}
|
|
649
|
+
// React synthetic events bubble through portals via the REACT tree, so a
|
|
650
|
+
// keydown inside this popover — or inside a Modal/Alert opened from it,
|
|
651
|
+
// which is a React child even though it portals to document.body — would
|
|
652
|
+
// reach the TRIGGER's ancestors (e.g. a grid cell's Escape-cancels-edit
|
|
653
|
+
// onKeyDown) and let a lower layer act on a higher layer's keys, on
|
|
654
|
+
// keydown, before any keyup layering logic runs. Keyboard sibling of the
|
|
655
|
+
// click curtain above. Keyup deliberately keeps flowing: RN-web Modal
|
|
656
|
+
// closes on a document-level keyup listener, and this popover's own
|
|
657
|
+
// Escape handling is a document capture keyup — neither must be starved.
|
|
658
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
629
659
|
>
|
|
630
660
|
{small && (
|
|
631
661
|
<View
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, test, beforeEach } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
hasModalLayerAbove,
|
|
5
|
+
isInLayerAbove,
|
|
6
|
+
snapshotOpenModalLayers,
|
|
7
|
+
} from "./popover_layers";
|
|
8
|
+
|
|
9
|
+
function addModal(): HTMLElement {
|
|
10
|
+
const modal = document.createElement("div");
|
|
11
|
+
modal.setAttribute("aria-modal", "true");
|
|
12
|
+
const inner = document.createElement("button");
|
|
13
|
+
modal.appendChild(inner);
|
|
14
|
+
document.body.appendChild(modal);
|
|
15
|
+
return modal;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function addPopover(level: number): HTMLElement {
|
|
19
|
+
const popover = document.createElement("div");
|
|
20
|
+
popover.setAttribute("data-popover", "true");
|
|
21
|
+
popover.setAttribute("data-popover-level", String(level));
|
|
22
|
+
const inner = document.createElement("button");
|
|
23
|
+
popover.appendChild(inner);
|
|
24
|
+
document.body.appendChild(popover);
|
|
25
|
+
return popover;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
document.body.innerHTML = "";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("isInLayerAbove", () => {
|
|
33
|
+
test("plain page node is not above", () => {
|
|
34
|
+
const el = document.createElement("div");
|
|
35
|
+
document.body.appendChild(el);
|
|
36
|
+
expect(isInLayerAbove(el, 0, new Set())).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("document itself (page scroll target) is not above", () => {
|
|
40
|
+
expect(isInLayerAbove(document, 0, new Set())).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("text node resolves through its parent element", () => {
|
|
44
|
+
const modal = addModal();
|
|
45
|
+
const text = document.createTextNode("hello");
|
|
46
|
+
modal.appendChild(text);
|
|
47
|
+
expect(isInLayerAbove(text, 0, new Set())).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("node inside a deeper nested popover is above", () => {
|
|
51
|
+
const nested = addPopover(1);
|
|
52
|
+
expect(isInLayerAbove(nested.firstChild as Node, 0, new Set())).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("node inside a same-level popover is not above", () => {
|
|
56
|
+
const sibling = addPopover(0);
|
|
57
|
+
expect(isInLayerAbove(sibling.firstChild as Node, 0, new Set())).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("node inside a modal opened AFTER the popover is above (file preview)", () => {
|
|
61
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
62
|
+
const preview = addModal();
|
|
63
|
+
expect(isInLayerAbove(preview.firstChild as Node, 0, snapshot)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("node inside the modal the popover LIVES IN is not above (drawer scroll dismisses)", () => {
|
|
67
|
+
const drawer = addModal();
|
|
68
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
69
|
+
expect(isInLayerAbove(drawer.firstChild as Node, 0, snapshot)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("stacked modals: only the one opened after the popover is above", () => {
|
|
73
|
+
const drawer = addModal();
|
|
74
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
75
|
+
const preview = addModal();
|
|
76
|
+
expect(isInLayerAbove(drawer.firstChild as Node, 0, snapshot)).toBe(false);
|
|
77
|
+
expect(isInLayerAbove(preview.firstChild as Node, 0, snapshot)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("null snapshot treats any modal ancestor as above", () => {
|
|
81
|
+
const modal = addModal();
|
|
82
|
+
expect(isInLayerAbove(modal.firstChild as Node, 0, null)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("hasModalLayerAbove", () => {
|
|
87
|
+
test("false with no modals", () => {
|
|
88
|
+
expect(hasModalLayerAbove(document, new Set())).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("false while only pre-existing modals are open (popover in a drawer)", () => {
|
|
92
|
+
addModal();
|
|
93
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
94
|
+
expect(hasModalLayerAbove(document, snapshot)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("true while a modal opened after the popover is open, false after it closes", () => {
|
|
98
|
+
addModal();
|
|
99
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
100
|
+
const preview = addModal();
|
|
101
|
+
expect(hasModalLayerAbove(document, snapshot)).toBe(true);
|
|
102
|
+
preview.remove();
|
|
103
|
+
expect(hasModalLayerAbove(document, snapshot)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a reopened modal is a NEW element and counts as above", () => {
|
|
107
|
+
const snapshot = snapshotOpenModalLayers(document);
|
|
108
|
+
const first = addModal();
|
|
109
|
+
first.remove();
|
|
110
|
+
addModal();
|
|
111
|
+
expect(hasModalLayerAbove(document, snapshot)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Layer-aware "outside" test for the non-modal anchored Popover.
|
|
2
|
+
//
|
|
3
|
+
// The popover dismisses on ambient signals — outside clicks, page scrolls,
|
|
4
|
+
// Escape. "Outside" cannot be a DOM-containment test alone: overlays the
|
|
5
|
+
// popover opens FROM WITHIN (a file-preview Modal, an Alert confirm, a nested
|
|
6
|
+
// popover) are React children but DOM SIBLINGS — they portal to document.body
|
|
7
|
+
// or a PortalHost — so containment misreads interactions inside them as "the
|
|
8
|
+
// user moved on" and dismisses the popover, unmounting the very overlay being
|
|
9
|
+
// used. Per WAI-ARIA, aria-modal content blocks interaction with everything
|
|
10
|
+
// beneath it, so an event inside such a layer says nothing about the layers
|
|
11
|
+
// underneath.
|
|
12
|
+
//
|
|
13
|
+
// A modal layer is ABOVE the popover exactly when it appeared AFTER the
|
|
14
|
+
// popover opened — it can only have been opened from within. Anything already
|
|
15
|
+
// open at that moment (the Dialog/Drawer the popover lives in) is part of the
|
|
16
|
+
// page: scrolling a drawer body moves the popover's anchor and must still
|
|
17
|
+
// dismiss. Callers snapshot the open modal set at open time and pass it in.
|
|
18
|
+
|
|
19
|
+
export function snapshotOpenModalLayers(doc: Document): ReadonlySet<Element> {
|
|
20
|
+
return new Set(doc.querySelectorAll('[aria-modal="true"]'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True when `node` sits inside an overlay stacked above the popover: a deeper
|
|
25
|
+
* nested popover, or a modal layer that opened after the popover did.
|
|
26
|
+
* `modalsAtOpen` null (no snapshot yet) is treated as "nothing was open".
|
|
27
|
+
*/
|
|
28
|
+
export function isInLayerAbove(
|
|
29
|
+
node: Node,
|
|
30
|
+
popoverLevel: number,
|
|
31
|
+
modalsAtOpen: ReadonlySet<Element> | null,
|
|
32
|
+
): boolean {
|
|
33
|
+
const el = node instanceof Element ? node : node.parentElement;
|
|
34
|
+
if (!el) return false;
|
|
35
|
+
const popoverHit = el.closest("[data-popover]");
|
|
36
|
+
if (popoverHit && Number(popoverHit.getAttribute("data-popover-level") ?? "0") > popoverLevel) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const modalHit = el.closest('[aria-modal="true"]');
|
|
40
|
+
return modalHit !== null && !(modalsAtOpen?.has(modalHit) ?? false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True while any modal layer opened after the popover is still open. The
|
|
45
|
+
* popover's Escape handler yields then: RN-web's own document keyup listener
|
|
46
|
+
* closes the topmost modal, and stopping the event at the popover would
|
|
47
|
+
* instead close the popover and unmount that modal with it.
|
|
48
|
+
*/
|
|
49
|
+
export function hasModalLayerAbove(
|
|
50
|
+
doc: Document,
|
|
51
|
+
modalsAtOpen: ReadonlySet<Element> | null,
|
|
52
|
+
): boolean {
|
|
53
|
+
for (const modal of doc.querySelectorAll('[aria-modal="true"]')) {
|
|
54
|
+
if (!(modalsAtOpen?.has(modal) ?? false)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|