@unofficialbox/box-open-elements 0.8.0 → 0.10.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/dist/components/overlays/command-palette.d.ts +1 -0
- package/dist/components/overlays/command-palette.js +42 -3
- package/dist/components/overlays/context-menu.js +11 -1
- package/dist/components/overlays/dialog.d.ts +1 -0
- package/dist/components/overlays/dialog.js +43 -3
- package/dist/components/overlays/drawer.d.ts +16 -5
- package/dist/components/overlays/drawer.js +66 -38
- package/dist/components/overlays/guide-tooltip.js +15 -1
- package/dist/components/overlays/popover.js +16 -1
- package/dist/components/overlays/shortcuts-overlay.d.ts +1 -0
- package/dist/components/overlays/shortcuts-overlay.js +40 -3
- package/dist/components/overlays/tooltip.js +15 -1
- package/dist/foundations/overlay/index.d.ts +1 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/top-layer.d.ts +62 -0
- package/dist/foundations/overlay/top-layer.js +95 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { groupCommandMatches, isCommandDescriptorRecord, matchCommands, splitCommandLabel, } from "./command-types.js";
|
|
2
2
|
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
3
4
|
import { FocusRestore, trapTabKey } from "../../foundations/a11y/index.js";
|
|
4
5
|
import { boeOverlay, boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
5
6
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
@@ -19,6 +20,10 @@ const elementStyles = `
|
|
|
19
20
|
display: contents;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/* A <dialog> promoted with showModal() — see
|
|
24
|
+
foundations/overlay/top-layer.ts. The top layer keeps the scrim above
|
|
25
|
+
the page without moving the host node. UA border/margin/max-* reset
|
|
26
|
+
because this is the full-viewport scrim, not a centred card. */
|
|
22
27
|
[part="backdrop"] {
|
|
23
28
|
position: fixed;
|
|
24
29
|
inset: 0;
|
|
@@ -28,6 +33,24 @@ const elementStyles = `
|
|
|
28
33
|
padding-block-start: 12vh;
|
|
29
34
|
background: ${boeOverlay.modalBackdrop};
|
|
30
35
|
z-index: 1000;
|
|
36
|
+
border: 0;
|
|
37
|
+
margin: 0;
|
|
38
|
+
max-width: none;
|
|
39
|
+
max-height: none;
|
|
40
|
+
width: auto;
|
|
41
|
+
height: auto;
|
|
42
|
+
color: inherit;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* The scrim paints itself; the UA pseudo must not double it up. */
|
|
46
|
+
[part="backdrop"]::backdrop {
|
|
47
|
+
background: transparent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Without showModal support the element stays hidden, which is the
|
|
51
|
+
correct closed state anyway. */
|
|
52
|
+
[part="backdrop"]:not([open]) {
|
|
53
|
+
display: none;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
[part="palette"] {
|
|
@@ -185,6 +208,7 @@ export class CommandPalette extends BaseElement {
|
|
|
185
208
|
return ["commands", "hide-disabled", "hotkey", "open", "placeholder"];
|
|
186
209
|
}
|
|
187
210
|
hostEl;
|
|
211
|
+
backdropEl = null;
|
|
188
212
|
focusRestore = new FocusRestore();
|
|
189
213
|
openValue = false;
|
|
190
214
|
wasOpen = false;
|
|
@@ -449,6 +473,9 @@ export class CommandPalette extends BaseElement {
|
|
|
449
473
|
this.openValue = this.open;
|
|
450
474
|
if (!this.openValue) {
|
|
451
475
|
const wasOpen = this.wasOpen;
|
|
476
|
+
// Leave the top layer before the element is discarded, not after.
|
|
477
|
+
dismissModal(this.backdropEl);
|
|
478
|
+
this.backdropEl = null;
|
|
452
479
|
this.hostEl.innerHTML = "";
|
|
453
480
|
this.rankedMatches = [];
|
|
454
481
|
this.wasOpen = false;
|
|
@@ -486,8 +513,8 @@ export class CommandPalette extends BaseElement {
|
|
|
486
513
|
const activeId = this.rankedMatches.length > 0 ? `command-option-${String(this.activeIndex)}` : "";
|
|
487
514
|
if (justOpened || !this.hostEl.querySelector('[part="palette"]')) {
|
|
488
515
|
this.hostEl.innerHTML = `
|
|
489
|
-
<
|
|
490
|
-
<div part="palette"
|
|
516
|
+
<dialog part="backdrop" aria-label="Command palette">
|
|
517
|
+
<div part="palette">
|
|
491
518
|
<div part="search-row">
|
|
492
519
|
<span part="search-icon" aria-hidden="true">⌕</span>
|
|
493
520
|
<input
|
|
@@ -511,9 +538,21 @@ export class CommandPalette extends BaseElement {
|
|
|
511
538
|
<span><span part="footer-key">esc</span> close</span>
|
|
512
539
|
</div>
|
|
513
540
|
</div>
|
|
514
|
-
</
|
|
541
|
+
</dialog>
|
|
515
542
|
`;
|
|
516
543
|
}
|
|
544
|
+
const freshScrim = this.backdropEl === null;
|
|
545
|
+
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
546
|
+
if (freshScrim && this.backdropEl) {
|
|
547
|
+
// A modal dialog closes itself on Escape and fires `cancel`; route it
|
|
548
|
+
// through the component's own dismissal so `open` stays in sync.
|
|
549
|
+
this.backdropEl.addEventListener("cancel", event => {
|
|
550
|
+
event.preventDefault();
|
|
551
|
+
this.dispatchEvent(new CustomEvent("dismissed", { bubbles: true, composed: true }));
|
|
552
|
+
this.open = false;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
promoteModal(this.backdropEl);
|
|
517
556
|
const input = this.hostEl.querySelector('[part="search"]');
|
|
518
557
|
const results = this.hostEl.querySelector('[part="results"]');
|
|
519
558
|
const empty = this.hostEl.querySelector('[part="empty"]');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { dismissPopover, promotePopover } from "../../foundations/overlay/index.js";
|
|
2
3
|
import { FocusRestore, nextRovingIndex } from "../../foundations/a11y/index.js";
|
|
3
4
|
import { boeOverlay, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
5
|
import { resolvePosition } from "../../foundations/overlay/index.js";
|
|
@@ -19,7 +20,12 @@ const contextMenuStyles = `
|
|
|
19
20
|
display: contents;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/* display is stated explicitly because the element carries
|
|
24
|
+
popover="manual": the UA sheet hides a popover that is not open, and an
|
|
25
|
+
author declaration outranks it. If showPopover() is unavailable or fails,
|
|
26
|
+
this still renders as before and only the top layer is lost. */
|
|
22
27
|
[part="surface"] {
|
|
28
|
+
display: grid;
|
|
23
29
|
position: fixed;
|
|
24
30
|
z-index: 40;
|
|
25
31
|
inset-block-start: 0;
|
|
@@ -136,7 +142,7 @@ export class ContextMenu extends BaseElement {
|
|
|
136
142
|
this.shadowRoot.innerHTML = `
|
|
137
143
|
<style>${contextMenuStyles}</style>
|
|
138
144
|
<div part="target"><slot></slot></div>
|
|
139
|
-
<div part="surface" role="menu" hidden></div>
|
|
145
|
+
<div part="surface" popover="manual" role="menu" hidden></div>
|
|
140
146
|
`;
|
|
141
147
|
this.targetEl = this.shadowRoot.querySelector('[part="target"]');
|
|
142
148
|
this.surfaceEl = this.shadowRoot.querySelector('[part="surface"]');
|
|
@@ -194,6 +200,9 @@ export class ContextMenu extends BaseElement {
|
|
|
194
200
|
openAt(clientX, clientY) {
|
|
195
201
|
this.renderItems();
|
|
196
202
|
this.surfaceEl.hidden = false;
|
|
203
|
+
// Top layer keeps the menu above the page even inside an ancestor with a
|
|
204
|
+
// transform/filter/contain, which position:fixed alone cannot escape.
|
|
205
|
+
promotePopover(this.surfaceEl);
|
|
197
206
|
const { width, height } = this.surfaceEl.getBoundingClientRect();
|
|
198
207
|
const { x, y } = resolvePosition({ top: clientY, left: clientX, width: 0, height: 0 }, { width, height }, { width: window.innerWidth, height: window.innerHeight }, { placement: { side: "bottom", align: "start" }, offset: 0 });
|
|
199
208
|
this.surfaceEl.style.left = `${Math.round(x)}px`;
|
|
@@ -209,6 +218,7 @@ export class ContextMenu extends BaseElement {
|
|
|
209
218
|
return;
|
|
210
219
|
this.openValue = false;
|
|
211
220
|
this.surfaceEl.hidden = true;
|
|
221
|
+
dismissPopover(this.surfaceEl);
|
|
212
222
|
this.unbindDocument();
|
|
213
223
|
this.dispatchEvent(new CustomEvent("open-changed", { bubbles: true, composed: true, detail: { open: false } }));
|
|
214
224
|
this.focusRestore.restore();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
2
3
|
import { FocusRestore, trapTabKey } from "../../foundations/a11y/index.js";
|
|
3
4
|
import { boeControl, boeOverlay, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
4
5
|
import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
|
|
@@ -9,6 +10,12 @@ const dialogStyles = `
|
|
|
9
10
|
font: inherit;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/* A <dialog> promoted with showModal(), not a plain box — see
|
|
14
|
+
foundations/overlay/top-layer.ts. The top layer keeps the scrim above the
|
|
15
|
+
page without moving the host node, so an ancestor with a transform,
|
|
16
|
+
filter, or contain cannot clip it. The UA sheet's border/padding/margin and
|
|
17
|
+
max-* are reset because this element is the full-viewport scrim rather
|
|
18
|
+
than a centred card. */
|
|
12
19
|
[part="backdrop"] {
|
|
13
20
|
position: fixed;
|
|
14
21
|
inset: 0;
|
|
@@ -17,6 +24,24 @@ const dialogStyles = `
|
|
|
17
24
|
display: grid;
|
|
18
25
|
place-items: center;
|
|
19
26
|
padding: ${boeOverlay.modalPadding};
|
|
27
|
+
border: 0;
|
|
28
|
+
margin: 0;
|
|
29
|
+
max-width: none;
|
|
30
|
+
max-height: none;
|
|
31
|
+
width: auto;
|
|
32
|
+
height: auto;
|
|
33
|
+
color: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The scrim paints itself; the UA pseudo must not double it up. */
|
|
37
|
+
[part="backdrop"]::backdrop {
|
|
38
|
+
background: transparent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Without showModal support the element stays hidden, which is the correct
|
|
42
|
+
closed state anyway. */
|
|
43
|
+
[part="backdrop"]:not([open]) {
|
|
44
|
+
display: none;
|
|
20
45
|
}
|
|
21
46
|
|
|
22
47
|
[part="dialog"] {
|
|
@@ -116,6 +141,7 @@ export class Dialog extends BaseElement {
|
|
|
116
141
|
hostEl;
|
|
117
142
|
titleEl = null;
|
|
118
143
|
descriptionEl = null;
|
|
144
|
+
backdropEl = null;
|
|
119
145
|
confirmEl = null;
|
|
120
146
|
focusRestore = new FocusRestore();
|
|
121
147
|
get open() {
|
|
@@ -242,6 +268,9 @@ export class Dialog extends BaseElement {
|
|
|
242
268
|
if (!this.openValue) {
|
|
243
269
|
const wasOpen = this.wasOpen;
|
|
244
270
|
this.setScrollLock(false);
|
|
271
|
+
// Leave the top layer before the element is discarded, not after.
|
|
272
|
+
dismissModal(this.backdropEl);
|
|
273
|
+
this.backdropEl = null;
|
|
245
274
|
this.hostEl.innerHTML = "";
|
|
246
275
|
this.titleEl = null;
|
|
247
276
|
this.descriptionEl = null;
|
|
@@ -261,8 +290,8 @@ export class Dialog extends BaseElement {
|
|
|
261
290
|
if (!this.hostEl.querySelector('[part="dialog"]')) {
|
|
262
291
|
this.hostEl.innerHTML = `
|
|
263
292
|
<style>${dialogStyles}</style>
|
|
264
|
-
<
|
|
265
|
-
<section part="dialog"
|
|
293
|
+
<dialog part="backdrop" aria-labelledby="dialog-title">
|
|
294
|
+
<section part="dialog" tabindex="-1" data-size="${this.size}">
|
|
266
295
|
<header part="header">
|
|
267
296
|
<h2 id="dialog-title"></h2>
|
|
268
297
|
</header>
|
|
@@ -273,9 +302,20 @@ export class Dialog extends BaseElement {
|
|
|
273
302
|
<button type="button" part="confirm"></button>
|
|
274
303
|
</footer>
|
|
275
304
|
</section>
|
|
276
|
-
</
|
|
305
|
+
</dialog>
|
|
277
306
|
`;
|
|
278
307
|
}
|
|
308
|
+
const freshScrim = this.backdropEl === null;
|
|
309
|
+
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
310
|
+
if (freshScrim && this.backdropEl) {
|
|
311
|
+
// A modal dialog closes itself on Escape and fires `cancel`. Left alone
|
|
312
|
+
// that would bypass close(), leaving `open` true with the dialog gone.
|
|
313
|
+
this.backdropEl.addEventListener("cancel", event => {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
this.close();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
promoteModal(this.backdropEl);
|
|
279
319
|
this.titleEl = this.hostEl.querySelector("#dialog-title");
|
|
280
320
|
this.descriptionEl = this.hostEl.querySelector('[part="description"]');
|
|
281
321
|
this.confirmEl = this.hostEl.querySelector('[part="confirm"]');
|
|
@@ -9,8 +9,6 @@ export declare class Drawer extends BaseElement {
|
|
|
9
9
|
private openValue;
|
|
10
10
|
private wasOpen;
|
|
11
11
|
private readonly focusRestore;
|
|
12
|
-
private placeholder;
|
|
13
|
-
private portaled;
|
|
14
12
|
private hostEl;
|
|
15
13
|
private backdropEl;
|
|
16
14
|
private drawerEl;
|
|
@@ -30,12 +28,25 @@ export declare class Drawer extends BaseElement {
|
|
|
30
28
|
set busy(value: boolean);
|
|
31
29
|
get heading(): string;
|
|
32
30
|
set heading(value: string);
|
|
33
|
-
connectedCallback(): void;
|
|
34
31
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
35
32
|
show(): void;
|
|
36
33
|
close(): void;
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
disconnectedCallback(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Promote the scrim into the top layer.
|
|
37
|
+
*
|
|
38
|
+
* `showModal` is the whole point of the `<dialog>`: it renders outside the
|
|
39
|
+
* normal flow, so the drawer covers the viewport without the host node being
|
|
40
|
+
* moved anywhere. The previous implementation relocated the host to
|
|
41
|
+
* `document.body`, which worked visually and broke every framework that owns
|
|
42
|
+
* the node — React unmounting a tree containing an open drawer threw
|
|
43
|
+
* `NotFoundError`, because the node it tried to remove was no longer its
|
|
44
|
+
* child.
|
|
45
|
+
*
|
|
46
|
+
* The guards live in the shared foundation, which every overlay now uses.
|
|
47
|
+
*/
|
|
48
|
+
private promoteTopLayer;
|
|
49
|
+
private dismissTopLayer;
|
|
39
50
|
protected renderTemplate(): void;
|
|
40
51
|
/**
|
|
41
52
|
* The unsaved-changes guard, host-owned: `dismiss` is cancelable, and a
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { FocusRestore, trapTabKey } from "../../foundations/a11y/index.js";
|
|
3
|
+
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
3
4
|
import { boeControl, boeOverlay, boePanel, boeRadius, boeSpace, } from "../../foundations/geometry/index.js";
|
|
4
5
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
5
6
|
const DEFAULT_TAG_NAME = "box-drawer";
|
|
@@ -9,12 +10,39 @@ const drawerStyles = `
|
|
|
9
10
|
font: inherit;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/* A <dialog> promoted with showModal(), not a plain box. The top layer is
|
|
14
|
+
what lets the drawer cover the page without the host node being moved
|
|
15
|
+
there: it renders outside the normal flow, so an ancestor with a
|
|
16
|
+
transform, filter, or contain — which would otherwise make position:fixed
|
|
17
|
+
resolve against that ancestor instead of the viewport — cannot clip it.
|
|
18
|
+
The UA sheet's border, padding, margin, and max-* are reset because this
|
|
19
|
+
element is the full-viewport scrim, not a centred card. */
|
|
12
20
|
[part="backdrop"] {
|
|
13
21
|
position: fixed;
|
|
14
22
|
inset: 0;
|
|
15
23
|
display: grid;
|
|
16
24
|
z-index: 1200;
|
|
17
25
|
background: ${boeOverlay.modalBackdrop};
|
|
26
|
+
border: 0;
|
|
27
|
+
padding: 0;
|
|
28
|
+
margin: 0;
|
|
29
|
+
max-width: none;
|
|
30
|
+
max-height: none;
|
|
31
|
+
width: auto;
|
|
32
|
+
height: auto;
|
|
33
|
+
color: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The scrim is painted by the element itself, so the UA backdrop pseudo
|
|
37
|
+
must not double it up. */
|
|
38
|
+
[part="backdrop"]::backdrop {
|
|
39
|
+
background: transparent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Only an open dialog participates in layout; without showModal support the
|
|
43
|
+
element stays display:none, which is the correct closed state anyway. */
|
|
44
|
+
[part="backdrop"]:not([open]) {
|
|
45
|
+
display: none;
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
[part="drawer"] {
|
|
@@ -178,8 +206,6 @@ export class Drawer extends BaseElement {
|
|
|
178
206
|
openValue = false;
|
|
179
207
|
wasOpen = false;
|
|
180
208
|
focusRestore = new FocusRestore();
|
|
181
|
-
placeholder = null;
|
|
182
|
-
portaled = false;
|
|
183
209
|
hostEl;
|
|
184
210
|
backdropEl = null;
|
|
185
211
|
drawerEl = null;
|
|
@@ -235,21 +261,9 @@ export class Drawer extends BaseElement {
|
|
|
235
261
|
set heading(value) {
|
|
236
262
|
this.setAttribute("heading", value);
|
|
237
263
|
}
|
|
238
|
-
connectedCallback() {
|
|
239
|
-
if (this.openValue) {
|
|
240
|
-
this.portalToBody();
|
|
241
|
-
}
|
|
242
|
-
super.connectedCallback();
|
|
243
|
-
}
|
|
244
264
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
245
265
|
if (name === "open") {
|
|
246
266
|
this.openValue = this.hasAttribute("open");
|
|
247
|
-
if (this.openValue) {
|
|
248
|
-
this.portalToBody();
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
this.restoreFromPortal();
|
|
252
|
-
}
|
|
253
267
|
}
|
|
254
268
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
255
269
|
}
|
|
@@ -258,29 +272,31 @@ export class Drawer extends BaseElement {
|
|
|
258
272
|
}
|
|
259
273
|
close() {
|
|
260
274
|
this.open = false;
|
|
261
|
-
this.restoreFromPortal();
|
|
262
275
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!parent || parent === this.ownerDocument.body) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
this.placeholder = this.ownerDocument.createComment("box-drawer-placeholder");
|
|
272
|
-
parent.insertBefore(this.placeholder, this);
|
|
273
|
-
this.ownerDocument.body.append(this);
|
|
274
|
-
this.portaled = true;
|
|
276
|
+
disconnectedCallback() {
|
|
277
|
+
// A dialog removed from the document while modal leaves the top layer
|
|
278
|
+
// behind it in some engines; closing first keeps that bookkeeping honest.
|
|
279
|
+
this.dismissTopLayer();
|
|
280
|
+
super.disconnectedCallback();
|
|
275
281
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Promote the scrim into the top layer.
|
|
284
|
+
*
|
|
285
|
+
* `showModal` is the whole point of the `<dialog>`: it renders outside the
|
|
286
|
+
* normal flow, so the drawer covers the viewport without the host node being
|
|
287
|
+
* moved anywhere. The previous implementation relocated the host to
|
|
288
|
+
* `document.body`, which worked visually and broke every framework that owns
|
|
289
|
+
* the node — React unmounting a tree containing an open drawer threw
|
|
290
|
+
* `NotFoundError`, because the node it tried to remove was no longer its
|
|
291
|
+
* child.
|
|
292
|
+
*
|
|
293
|
+
* The guards live in the shared foundation, which every overlay now uses.
|
|
294
|
+
*/
|
|
295
|
+
promoteTopLayer() {
|
|
296
|
+
promoteModal(this.backdropEl);
|
|
297
|
+
}
|
|
298
|
+
dismissTopLayer() {
|
|
299
|
+
dismissModal(this.backdropEl);
|
|
284
300
|
}
|
|
285
301
|
renderTemplate() {
|
|
286
302
|
if (!this.shadowRoot) {
|
|
@@ -343,6 +359,8 @@ export class Drawer extends BaseElement {
|
|
|
343
359
|
}
|
|
344
360
|
if (!this.openValue) {
|
|
345
361
|
const wasOpen = this.wasOpen;
|
|
362
|
+
// Leave the top layer before the element is discarded, not after.
|
|
363
|
+
this.dismissTopLayer();
|
|
346
364
|
this.hostEl.innerHTML = "";
|
|
347
365
|
this.backdropEl = null;
|
|
348
366
|
this.drawerEl = null;
|
|
@@ -362,8 +380,8 @@ export class Drawer extends BaseElement {
|
|
|
362
380
|
if (!this.hostEl.querySelector('[part="drawer"]')) {
|
|
363
381
|
this.hostEl.innerHTML = `
|
|
364
382
|
<style>${drawerStyles}</style>
|
|
365
|
-
<
|
|
366
|
-
<aside part="drawer"
|
|
383
|
+
<dialog part="backdrop" aria-labelledby="drawer-title">
|
|
384
|
+
<aside part="drawer">
|
|
367
385
|
<header part="header">
|
|
368
386
|
<div part="meta">
|
|
369
387
|
<h2 id="drawer-title"></h2>
|
|
@@ -380,7 +398,7 @@ export class Drawer extends BaseElement {
|
|
|
380
398
|
<slot name="footer"></slot>
|
|
381
399
|
</footer>
|
|
382
400
|
</aside>
|
|
383
|
-
</
|
|
401
|
+
</dialog>
|
|
384
402
|
`;
|
|
385
403
|
// The footer row only exists when the host slots one: an empty sticky
|
|
386
404
|
// bar would just eat drawer height.
|
|
@@ -388,8 +406,18 @@ export class Drawer extends BaseElement {
|
|
|
388
406
|
footerSlot?.addEventListener("slotchange", () => {
|
|
389
407
|
this.syncFooter();
|
|
390
408
|
});
|
|
409
|
+
// A modal dialog closes itself on Escape and fires `cancel`. Left alone
|
|
410
|
+
// that would bypass the cancelable `dismiss` contract entirely — a host
|
|
411
|
+
// calling preventDefault() would keep `open` true while the dialog had
|
|
412
|
+
// already gone. Route it through the same guard as every other dismissal.
|
|
413
|
+
const dialogEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
414
|
+
dialogEl?.addEventListener("cancel", event => {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
this.requestDismiss("escape");
|
|
417
|
+
});
|
|
391
418
|
}
|
|
392
419
|
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
420
|
+
this.promoteTopLayer();
|
|
393
421
|
this.drawerEl = this.hostEl.querySelector('[part="drawer"]');
|
|
394
422
|
this.titleEl = this.hostEl.querySelector("#drawer-title");
|
|
395
423
|
this.descriptionEl = this.hostEl.querySelector('[part="description"]');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { dismissPopover, promotePopover } from "../../foundations/overlay/index.js";
|
|
2
3
|
import { boeControl, boeOverlay, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
4
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
4
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
@@ -13,7 +14,12 @@ const guideTooltipStyles = `
|
|
|
13
14
|
|
|
14
15
|
/* Positioned by JS (foundations/overlay) as a viewport-fixed callout, so it
|
|
15
16
|
escapes ancestor overflow and flips/shifts to stay on-screen. */
|
|
17
|
+
/* display is stated explicitly because the element carries
|
|
18
|
+
popover="manual": the UA sheet hides a popover that is not open, and an
|
|
19
|
+
author declaration outranks it. If showPopover() is unavailable or fails,
|
|
20
|
+
this still renders as before and only the top layer is lost. */
|
|
16
21
|
[part="callout"] {
|
|
22
|
+
display: block;
|
|
17
23
|
position: fixed;
|
|
18
24
|
inset-block-start: 0;
|
|
19
25
|
inset-inline-start: 0;
|
|
@@ -283,7 +289,7 @@ export class GuideTooltip extends BaseElement {
|
|
|
283
289
|
this.shadowRoot.innerHTML = `
|
|
284
290
|
<style>${guideTooltipStyles}</style>
|
|
285
291
|
<slot name="anchor"></slot>
|
|
286
|
-
<div part="callout" role="dialog" aria-labelledby="${this.headingId}" hidden>
|
|
292
|
+
<div part="callout" popover="manual" role="dialog" aria-labelledby="${this.headingId}" hidden>
|
|
287
293
|
<button type="button" part="close" aria-label="Close">
|
|
288
294
|
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
|
289
295
|
</button>
|
|
@@ -329,6 +335,14 @@ export class GuideTooltip extends BaseElement {
|
|
|
329
335
|
return;
|
|
330
336
|
}
|
|
331
337
|
this.calloutEl.hidden = !this.openValue;
|
|
338
|
+
// Top layer keeps it above the page even inside an ancestor with a
|
|
339
|
+
// transform/filter/contain, which position:fixed alone cannot escape.
|
|
340
|
+
if (this.openValue) {
|
|
341
|
+
promotePopover(this.calloutEl);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
dismissPopover(this.calloutEl);
|
|
345
|
+
}
|
|
332
346
|
const heading = this.heading;
|
|
333
347
|
this.headingEl.hidden = !heading;
|
|
334
348
|
this.headingEl.textContent = heading;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { dismissPopover, promotePopover } from "../../foundations/overlay/index.js";
|
|
2
3
|
import { FocusRestore, getTabbableElements } from "../../foundations/a11y/index.js";
|
|
3
4
|
import { boeControl, boeOverlay, boeRadius } from "../../foundations/geometry/index.js";
|
|
4
5
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
@@ -51,7 +52,13 @@ const popoverStyles = `
|
|
|
51
52
|
/* Positioned by JS (foundations/overlay) as position: fixed in viewport
|
|
52
53
|
coordinates — so it escapes ancestor overflow and flips/shifts to stay in
|
|
53
54
|
view, the job a portal does in box-ui-elements. */
|
|
55
|
+
/* display is stated explicitly, not left to the initial value, because the
|
|
56
|
+
element carries popover="manual": the UA sheet hides a popover that is not
|
|
57
|
+
open, and an author declaration outranks it. So if showPopover() is
|
|
58
|
+
unavailable or fails, the surface still renders exactly as before and only
|
|
59
|
+
the top-layer promotion is lost — see foundations/overlay/top-layer.ts. */
|
|
54
60
|
[part="surface"] {
|
|
61
|
+
display: block;
|
|
55
62
|
position: fixed;
|
|
56
63
|
z-index: 30;
|
|
57
64
|
inset-block-start: 0;
|
|
@@ -246,7 +253,7 @@ export class Popover extends BaseElement {
|
|
|
246
253
|
<style>${popoverStyles}</style>
|
|
247
254
|
<div part="container">
|
|
248
255
|
<button type="button" part="trigger" id="boe-popover-trigger" aria-haspopup="dialog" aria-controls="boe-popover-surface"></button>
|
|
249
|
-
<div part="surface" role="dialog" id="boe-popover-surface" aria-labelledby="boe-popover-trigger" tabindex="-1" hidden>
|
|
256
|
+
<div part="surface" popover="manual" role="dialog" id="boe-popover-surface" aria-labelledby="boe-popover-trigger" tabindex="-1" hidden>
|
|
250
257
|
<slot></slot>
|
|
251
258
|
</div>
|
|
252
259
|
</div>
|
|
@@ -288,6 +295,14 @@ export class Popover extends BaseElement {
|
|
|
288
295
|
const justOpened = this.openValue && !this.wasOpen;
|
|
289
296
|
const justClosed = !this.openValue && this.wasOpen;
|
|
290
297
|
this.surfaceEl.hidden = !this.openValue;
|
|
298
|
+
// Top layer keeps the surface above the page even inside an ancestor with
|
|
299
|
+
// a transform/filter/contain, which position:fixed alone cannot escape.
|
|
300
|
+
if (this.openValue) {
|
|
301
|
+
promotePopover(this.surfaceEl);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
dismissPopover(this.surfaceEl);
|
|
305
|
+
}
|
|
291
306
|
this.syncDocumentListeners();
|
|
292
307
|
if (this.openValue) {
|
|
293
308
|
this.startPositioning();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { groupShortcutCommands, isCommandDescriptorRecord, splitShortcutKeys, } from "./command-types.js";
|
|
2
2
|
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
3
4
|
import { FocusRestore, trapTabKey } from "../../foundations/a11y/index.js";
|
|
4
5
|
import { boeOverlay, boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
5
6
|
const DEFAULT_TAG_NAME = "box-shortcuts-overlay";
|
|
@@ -37,6 +38,10 @@ const elementStyles = `
|
|
|
37
38
|
display: contents;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/* A <dialog> promoted with showModal() — see
|
|
42
|
+
foundations/overlay/top-layer.ts. The top layer keeps the scrim above
|
|
43
|
+
the page without moving the host node. UA border/margin/max-* reset
|
|
44
|
+
because this is the full-viewport scrim, not a centred card. */
|
|
40
45
|
[part="backdrop"] {
|
|
41
46
|
position: fixed;
|
|
42
47
|
inset: 0;
|
|
@@ -46,6 +51,24 @@ const elementStyles = `
|
|
|
46
51
|
padding: 1rem;
|
|
47
52
|
background: ${boeOverlay.modalBackdrop};
|
|
48
53
|
z-index: 1000;
|
|
54
|
+
border: 0;
|
|
55
|
+
margin: 0;
|
|
56
|
+
max-width: none;
|
|
57
|
+
max-height: none;
|
|
58
|
+
width: auto;
|
|
59
|
+
height: auto;
|
|
60
|
+
color: inherit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* The scrim paints itself; the UA pseudo must not double it up. */
|
|
64
|
+
[part="backdrop"]::backdrop {
|
|
65
|
+
background: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Without showModal support the element stays hidden, which is the
|
|
69
|
+
correct closed state anyway. */
|
|
70
|
+
[part="backdrop"]:not([open]) {
|
|
71
|
+
display: none;
|
|
49
72
|
}
|
|
50
73
|
|
|
51
74
|
[part="sheet"] {
|
|
@@ -183,6 +206,7 @@ export class ShortcutsOverlay extends BaseElement {
|
|
|
183
206
|
return ["commands", "heading", "hotkey", "open"];
|
|
184
207
|
}
|
|
185
208
|
hostEl;
|
|
209
|
+
backdropEl = null;
|
|
186
210
|
focusRestore = new FocusRestore();
|
|
187
211
|
wasOpen = false;
|
|
188
212
|
commandsRaw = null;
|
|
@@ -333,6 +357,9 @@ export class ShortcutsOverlay extends BaseElement {
|
|
|
333
357
|
}
|
|
334
358
|
if (!this.open) {
|
|
335
359
|
const wasOpen = this.wasOpen;
|
|
360
|
+
// Leave the top layer before the element is discarded, not after.
|
|
361
|
+
dismissModal(this.backdropEl);
|
|
362
|
+
this.backdropEl = null;
|
|
336
363
|
this.hostEl.innerHTML = "";
|
|
337
364
|
this.wasOpen = false;
|
|
338
365
|
if (wasOpen) {
|
|
@@ -365,8 +392,8 @@ export class ShortcutsOverlay extends BaseElement {
|
|
|
365
392
|
})
|
|
366
393
|
.join("");
|
|
367
394
|
this.hostEl.innerHTML = `
|
|
368
|
-
<
|
|
369
|
-
<div part="sheet"
|
|
395
|
+
<dialog part="backdrop" aria-label="Keyboard shortcuts">
|
|
396
|
+
<div part="sheet">
|
|
370
397
|
<div part="header">
|
|
371
398
|
<h2 part="title">${escapeHtml(this.heading)}</h2>
|
|
372
399
|
<button type="button" part="close">Close</button>
|
|
@@ -375,8 +402,18 @@ export class ShortcutsOverlay extends BaseElement {
|
|
|
375
402
|
? `<ul part="groups" role="list">${sections}</ul>`
|
|
376
403
|
: `<div part="empty">No keyboard shortcuts are defined.</div>`}
|
|
377
404
|
</div>
|
|
378
|
-
</
|
|
405
|
+
</dialog>
|
|
379
406
|
`;
|
|
407
|
+
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
408
|
+
if (this.backdropEl) {
|
|
409
|
+
// A modal dialog closes itself on Escape and fires `cancel`; route it
|
|
410
|
+
// through the component's own dismissal so state stays in sync.
|
|
411
|
+
this.backdropEl.addEventListener("cancel", event => {
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
this.dismiss();
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
promoteModal(this.backdropEl);
|
|
380
417
|
if (justOpened) {
|
|
381
418
|
this.hostEl.querySelector('[part="close"]')?.focus();
|
|
382
419
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { dismissPopover, promotePopover } from "../../foundations/overlay/index.js";
|
|
2
3
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
3
4
|
import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
|
|
4
5
|
import { parsePlacement, trackAnchor, } from "../../foundations/overlay/index.js";
|
|
@@ -40,7 +41,12 @@ const tooltipStyles = `
|
|
|
40
41
|
|
|
41
42
|
/* Positioned by JS (foundations/overlay) as position: fixed, so it escapes
|
|
42
43
|
ancestor overflow and flips/shifts to stay in the viewport. */
|
|
44
|
+
/* display is stated explicitly because the element carries
|
|
45
|
+
popover="manual": the UA sheet hides a popover that is not open, and an
|
|
46
|
+
author declaration outranks it. If showPopover() is unavailable or fails,
|
|
47
|
+
this still renders as before and only the top layer is lost. */
|
|
43
48
|
[part="tooltip"] {
|
|
49
|
+
display: block;
|
|
44
50
|
position: fixed;
|
|
45
51
|
inset-block-start: 0;
|
|
46
52
|
inset-inline-start: 0;
|
|
@@ -180,7 +186,7 @@ export class Tooltip extends BaseElement {
|
|
|
180
186
|
<span part="trigger-host">
|
|
181
187
|
<slot><button type="button" part="trigger">?</button></slot>
|
|
182
188
|
</span>
|
|
183
|
-
<div id="${this.tooltipId}" part="tooltip" role="tooltip" hidden><span part="label"></span><slot name="content"></slot></div>
|
|
189
|
+
<div id="${this.tooltipId}" part="tooltip" popover="manual" role="tooltip" hidden><span part="label"></span><slot name="content"></slot></div>
|
|
184
190
|
</span>
|
|
185
191
|
`;
|
|
186
192
|
this.triggerHostEl = this.shadowRoot.querySelector('[part="trigger-host"]');
|
|
@@ -232,6 +238,14 @@ export class Tooltip extends BaseElement {
|
|
|
232
238
|
this.tooltipEl.setAttribute("data-theme", theme);
|
|
233
239
|
}
|
|
234
240
|
this.tooltipEl.hidden = !this.openValue;
|
|
241
|
+
// Top layer keeps it above the page even inside an ancestor with a
|
|
242
|
+
// transform/filter/contain, which position:fixed alone cannot escape.
|
|
243
|
+
if (this.openValue) {
|
|
244
|
+
promotePopover(this.tooltipEl);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
dismissPopover(this.tooltipEl);
|
|
248
|
+
}
|
|
235
249
|
const trigger = this.resolveDescribedTrigger();
|
|
236
250
|
if (trigger === this.fallbackTriggerEl) {
|
|
237
251
|
this.fallbackTriggerEl.setAttribute("aria-label", this.triggerLabel);
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./positioning.js";
|
|
2
|
+
export { dismissModal, dismissPopover, promoteModal, promotePopover, } from "./top-layer.js";
|
|
2
3
|
export type { OverlayAlign, OverlayPlacement, OverlaySide, PositionOptions, PositionResult, Rect, Size, } from "./positioning.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-layer promotion for overlays.
|
|
3
|
+
*
|
|
4
|
+
* An overlay has to paint above the page, and `position: fixed` alone cannot
|
|
5
|
+
* promise that: an ancestor with a `transform`, `filter`, `perspective`,
|
|
6
|
+
* `contain`, or `will-change` becomes the containing block, and the overlay is
|
|
7
|
+
* positioned — and clipped — against that ancestor instead of the viewport.
|
|
8
|
+
*
|
|
9
|
+
* The old answer was to move the overlay's host node to `document.body`.
|
|
10
|
+
* `box-drawer` did exactly that and it broke every framework that owns the
|
|
11
|
+
* node: React unmounting a tree containing an open drawer threw
|
|
12
|
+
* `NotFoundError`, because the node it tried to remove was no longer its child.
|
|
13
|
+
*
|
|
14
|
+
* The top layer solves the same problem without touching the DOM tree. It is a
|
|
15
|
+
* rendering concern, so the host stays exactly where its framework put it.
|
|
16
|
+
*
|
|
17
|
+
* Two primitives, and the difference is modality rather than taste:
|
|
18
|
+
*
|
|
19
|
+
* - `showModal()` on a `<dialog>` — for overlays that own the screen while they
|
|
20
|
+
* are up, with a scrim, a focus trap, and the rest of the page inert.
|
|
21
|
+
* - `showPopover()` on a `[popover]` element — for anchored, non-modal surfaces
|
|
22
|
+
* (tooltips, menus, popovers) that must **not** trap focus or block the page.
|
|
23
|
+
*
|
|
24
|
+
* Using the modal primitive for an anchored surface would be a behaviour
|
|
25
|
+
* regression, not just an overreach, which is why these are separate calls.
|
|
26
|
+
*
|
|
27
|
+
* Every entry point is guarded rather than assumed. jsdom implements neither
|
|
28
|
+
* API, so the unit suite exercises everything around the promotion and the
|
|
29
|
+
* promotion itself is verified in a browser; and a detached or hidden element
|
|
30
|
+
* throws rather than opening, which is a normal state during teardown.
|
|
31
|
+
*/
|
|
32
|
+
/** An element that may be promotable as a modal dialog. */
|
|
33
|
+
type MaybeDialog = HTMLElement & {
|
|
34
|
+
showModal?: () => void;
|
|
35
|
+
close?: () => void;
|
|
36
|
+
open?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** An element that may be promotable as a popover. */
|
|
39
|
+
type MaybePopover = HTMLElement & {
|
|
40
|
+
showPopover?: () => void;
|
|
41
|
+
hidePopover?: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Promote a `<dialog>` into the top layer as a modal.
|
|
45
|
+
*
|
|
46
|
+
* Returns whether the promotion happened, so a caller can tell "not supported
|
|
47
|
+
* here" from "already open" if it needs to — most do not.
|
|
48
|
+
*/
|
|
49
|
+
export declare const promoteModal: (element: MaybeDialog | null) => boolean;
|
|
50
|
+
/** Close a modal `<dialog>`, leaving the top layer. */
|
|
51
|
+
export declare const dismissModal: (element: MaybeDialog | null) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Promote an element into the top layer as a **non-modal** popover.
|
|
54
|
+
*
|
|
55
|
+
* Callers are expected to set `popover="manual"` on the element: `auto`
|
|
56
|
+
* popovers light-dismiss and close each other, which fights components that
|
|
57
|
+
* already own their own dismissal rules.
|
|
58
|
+
*/
|
|
59
|
+
export declare const promotePopover: (element: MaybePopover | null) => boolean;
|
|
60
|
+
/** Hide a popover, leaving the top layer. */
|
|
61
|
+
export declare const dismissPopover: (element: MaybePopover | null) => void;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-layer promotion for overlays.
|
|
3
|
+
*
|
|
4
|
+
* An overlay has to paint above the page, and `position: fixed` alone cannot
|
|
5
|
+
* promise that: an ancestor with a `transform`, `filter`, `perspective`,
|
|
6
|
+
* `contain`, or `will-change` becomes the containing block, and the overlay is
|
|
7
|
+
* positioned — and clipped — against that ancestor instead of the viewport.
|
|
8
|
+
*
|
|
9
|
+
* The old answer was to move the overlay's host node to `document.body`.
|
|
10
|
+
* `box-drawer` did exactly that and it broke every framework that owns the
|
|
11
|
+
* node: React unmounting a tree containing an open drawer threw
|
|
12
|
+
* `NotFoundError`, because the node it tried to remove was no longer its child.
|
|
13
|
+
*
|
|
14
|
+
* The top layer solves the same problem without touching the DOM tree. It is a
|
|
15
|
+
* rendering concern, so the host stays exactly where its framework put it.
|
|
16
|
+
*
|
|
17
|
+
* Two primitives, and the difference is modality rather than taste:
|
|
18
|
+
*
|
|
19
|
+
* - `showModal()` on a `<dialog>` — for overlays that own the screen while they
|
|
20
|
+
* are up, with a scrim, a focus trap, and the rest of the page inert.
|
|
21
|
+
* - `showPopover()` on a `[popover]` element — for anchored, non-modal surfaces
|
|
22
|
+
* (tooltips, menus, popovers) that must **not** trap focus or block the page.
|
|
23
|
+
*
|
|
24
|
+
* Using the modal primitive for an anchored surface would be a behaviour
|
|
25
|
+
* regression, not just an overreach, which is why these are separate calls.
|
|
26
|
+
*
|
|
27
|
+
* Every entry point is guarded rather than assumed. jsdom implements neither
|
|
28
|
+
* API, so the unit suite exercises everything around the promotion and the
|
|
29
|
+
* promotion itself is verified in a browser; and a detached or hidden element
|
|
30
|
+
* throws rather than opening, which is a normal state during teardown.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Promote a `<dialog>` into the top layer as a modal.
|
|
34
|
+
*
|
|
35
|
+
* Returns whether the promotion happened, so a caller can tell "not supported
|
|
36
|
+
* here" from "already open" if it needs to — most do not.
|
|
37
|
+
*/
|
|
38
|
+
export const promoteModal = (element) => {
|
|
39
|
+
if (!element || typeof element.showModal !== "function" || element.open) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
element.showModal();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Detached, hidden, or already promoted by another path. The overlay still
|
|
48
|
+
// renders and behaves; it simply does not get the top layer.
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** Close a modal `<dialog>`, leaving the top layer. */
|
|
53
|
+
export const dismissModal = (element) => {
|
|
54
|
+
if (!element || typeof element.close !== "function" || !element.open) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
element.close();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Already closed or detached; nothing to undo.
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Promote an element into the top layer as a **non-modal** popover.
|
|
66
|
+
*
|
|
67
|
+
* Callers are expected to set `popover="manual"` on the element: `auto`
|
|
68
|
+
* popovers light-dismiss and close each other, which fights components that
|
|
69
|
+
* already own their own dismissal rules.
|
|
70
|
+
*/
|
|
71
|
+
export const promotePopover = (element) => {
|
|
72
|
+
if (!element || typeof element.showPopover !== "function") {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
element.showPopover();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Throws if already showing, or if the element is not connected.
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** Hide a popover, leaving the top layer. */
|
|
85
|
+
export const dismissPopover = (element) => {
|
|
86
|
+
if (!element || typeof element.hidePopover !== "function") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
element.hidePopover();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Not currently showing; nothing to undo.
|
|
94
|
+
}
|
|
95
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unofficialbox/box-open-elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://unofficialbox.github.io/box-open-elements",
|