@unofficialbox/box-open-elements 0.8.0 → 0.9.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.
|
@@ -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,27 @@ 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
|
+
* Guarded rather than assumed: jsdom implements neither `showModal` nor
|
|
47
|
+
* `showPopover`, so the unit suite exercises everything except the promotion
|
|
48
|
+
* itself, and that is verified in a browser.
|
|
49
|
+
*/
|
|
50
|
+
private promoteTopLayer;
|
|
51
|
+
private dismissTopLayer;
|
|
39
52
|
protected renderTemplate(): void;
|
|
40
53
|
/**
|
|
41
54
|
* The unsaved-changes guard, host-owned: `dismiss` is cancelable, and a
|
|
@@ -9,12 +9,39 @@ const drawerStyles = `
|
|
|
9
9
|
font: inherit;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* A <dialog> promoted with showModal(), not a plain box. The top layer is
|
|
13
|
+
what lets the drawer cover the page without the host node being moved
|
|
14
|
+
there: it renders outside the normal flow, so an ancestor with a
|
|
15
|
+
transform, filter, or contain — which would otherwise make position:fixed
|
|
16
|
+
resolve against that ancestor instead of the viewport — cannot clip it.
|
|
17
|
+
The UA sheet's border, padding, margin, and max-* are reset because this
|
|
18
|
+
element is the full-viewport scrim, not a centred card. */
|
|
12
19
|
[part="backdrop"] {
|
|
13
20
|
position: fixed;
|
|
14
21
|
inset: 0;
|
|
15
22
|
display: grid;
|
|
16
23
|
z-index: 1200;
|
|
17
24
|
background: ${boeOverlay.modalBackdrop};
|
|
25
|
+
border: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: 0;
|
|
28
|
+
max-width: none;
|
|
29
|
+
max-height: none;
|
|
30
|
+
width: auto;
|
|
31
|
+
height: auto;
|
|
32
|
+
color: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* The scrim is painted by the element itself, so the UA backdrop pseudo
|
|
36
|
+
must not double it up. */
|
|
37
|
+
[part="backdrop"]::backdrop {
|
|
38
|
+
background: transparent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Only an open dialog participates in layout; without showModal support the
|
|
42
|
+
element stays display:none, which is the correct closed state anyway. */
|
|
43
|
+
[part="backdrop"]:not([open]) {
|
|
44
|
+
display: none;
|
|
18
45
|
}
|
|
19
46
|
|
|
20
47
|
[part="drawer"] {
|
|
@@ -178,8 +205,6 @@ export class Drawer extends BaseElement {
|
|
|
178
205
|
openValue = false;
|
|
179
206
|
wasOpen = false;
|
|
180
207
|
focusRestore = new FocusRestore();
|
|
181
|
-
placeholder = null;
|
|
182
|
-
portaled = false;
|
|
183
208
|
hostEl;
|
|
184
209
|
backdropEl = null;
|
|
185
210
|
drawerEl = null;
|
|
@@ -235,21 +260,9 @@ export class Drawer extends BaseElement {
|
|
|
235
260
|
set heading(value) {
|
|
236
261
|
this.setAttribute("heading", value);
|
|
237
262
|
}
|
|
238
|
-
connectedCallback() {
|
|
239
|
-
if (this.openValue) {
|
|
240
|
-
this.portalToBody();
|
|
241
|
-
}
|
|
242
|
-
super.connectedCallback();
|
|
243
|
-
}
|
|
244
263
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
245
264
|
if (name === "open") {
|
|
246
265
|
this.openValue = this.hasAttribute("open");
|
|
247
|
-
if (this.openValue) {
|
|
248
|
-
this.portalToBody();
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
this.restoreFromPortal();
|
|
252
|
-
}
|
|
253
266
|
}
|
|
254
267
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
255
268
|
}
|
|
@@ -258,29 +271,48 @@ export class Drawer extends BaseElement {
|
|
|
258
271
|
}
|
|
259
272
|
close() {
|
|
260
273
|
this.open = false;
|
|
261
|
-
this.restoreFromPortal();
|
|
262
274
|
}
|
|
263
|
-
|
|
264
|
-
|
|
275
|
+
disconnectedCallback() {
|
|
276
|
+
// A dialog removed from the document while modal leaves the top layer
|
|
277
|
+
// behind it in some engines; closing first keeps that bookkeeping honest.
|
|
278
|
+
this.dismissTopLayer();
|
|
279
|
+
super.disconnectedCallback();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Promote the scrim into the top layer.
|
|
283
|
+
*
|
|
284
|
+
* `showModal` is the whole point of the `<dialog>`: it renders outside the
|
|
285
|
+
* normal flow, so the drawer covers the viewport without the host node being
|
|
286
|
+
* moved anywhere. The previous implementation relocated the host to
|
|
287
|
+
* `document.body`, which worked visually and broke every framework that owns
|
|
288
|
+
* the node — React unmounting a tree containing an open drawer threw
|
|
289
|
+
* `NotFoundError`, because the node it tried to remove was no longer its
|
|
290
|
+
* child.
|
|
291
|
+
*
|
|
292
|
+
* Guarded rather than assumed: jsdom implements neither `showModal` nor
|
|
293
|
+
* `showPopover`, so the unit suite exercises everything except the promotion
|
|
294
|
+
* itself, and that is verified in a browser.
|
|
295
|
+
*/
|
|
296
|
+
promoteTopLayer() {
|
|
297
|
+
const dialog = this.backdropEl;
|
|
298
|
+
if (!dialog || typeof dialog.showModal !== "function" || dialog.open) {
|
|
265
299
|
return;
|
|
266
300
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
301
|
+
// A dialog in a detached or hidden tree throws rather than opening.
|
|
302
|
+
try {
|
|
303
|
+
dialog.showModal();
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Not promotable here; the drawer still renders and behaves, it simply
|
|
307
|
+
// does not get the top layer.
|
|
270
308
|
}
|
|
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;
|
|
275
309
|
}
|
|
276
|
-
|
|
277
|
-
|
|
310
|
+
dismissTopLayer() {
|
|
311
|
+
const dialog = this.backdropEl;
|
|
312
|
+
if (!dialog || typeof dialog.close !== "function" || !dialog.open) {
|
|
278
313
|
return;
|
|
279
314
|
}
|
|
280
|
-
|
|
281
|
-
this.placeholder.remove();
|
|
282
|
-
this.placeholder = null;
|
|
283
|
-
this.portaled = false;
|
|
315
|
+
dialog.close();
|
|
284
316
|
}
|
|
285
317
|
renderTemplate() {
|
|
286
318
|
if (!this.shadowRoot) {
|
|
@@ -343,6 +375,8 @@ export class Drawer extends BaseElement {
|
|
|
343
375
|
}
|
|
344
376
|
if (!this.openValue) {
|
|
345
377
|
const wasOpen = this.wasOpen;
|
|
378
|
+
// Leave the top layer before the element is discarded, not after.
|
|
379
|
+
this.dismissTopLayer();
|
|
346
380
|
this.hostEl.innerHTML = "";
|
|
347
381
|
this.backdropEl = null;
|
|
348
382
|
this.drawerEl = null;
|
|
@@ -362,8 +396,8 @@ export class Drawer extends BaseElement {
|
|
|
362
396
|
if (!this.hostEl.querySelector('[part="drawer"]')) {
|
|
363
397
|
this.hostEl.innerHTML = `
|
|
364
398
|
<style>${drawerStyles}</style>
|
|
365
|
-
<
|
|
366
|
-
<aside part="drawer"
|
|
399
|
+
<dialog part="backdrop" aria-labelledby="drawer-title">
|
|
400
|
+
<aside part="drawer">
|
|
367
401
|
<header part="header">
|
|
368
402
|
<div part="meta">
|
|
369
403
|
<h2 id="drawer-title"></h2>
|
|
@@ -380,7 +414,7 @@ export class Drawer extends BaseElement {
|
|
|
380
414
|
<slot name="footer"></slot>
|
|
381
415
|
</footer>
|
|
382
416
|
</aside>
|
|
383
|
-
</
|
|
417
|
+
</dialog>
|
|
384
418
|
`;
|
|
385
419
|
// The footer row only exists when the host slots one: an empty sticky
|
|
386
420
|
// bar would just eat drawer height.
|
|
@@ -388,8 +422,18 @@ export class Drawer extends BaseElement {
|
|
|
388
422
|
footerSlot?.addEventListener("slotchange", () => {
|
|
389
423
|
this.syncFooter();
|
|
390
424
|
});
|
|
425
|
+
// A modal dialog closes itself on Escape and fires `cancel`. Left alone
|
|
426
|
+
// that would bypass the cancelable `dismiss` contract entirely — a host
|
|
427
|
+
// calling preventDefault() would keep `open` true while the dialog had
|
|
428
|
+
// already gone. Route it through the same guard as every other dismissal.
|
|
429
|
+
const dialogEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
430
|
+
dialogEl?.addEventListener("cancel", event => {
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
this.requestDismiss("escape");
|
|
433
|
+
});
|
|
391
434
|
}
|
|
392
435
|
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
436
|
+
this.promoteTopLayer();
|
|
393
437
|
this.drawerEl = this.hostEl.querySelector('[part="drawer"]');
|
|
394
438
|
this.titleEl = this.hostEl.querySelector("#drawer-title");
|
|
395
439
|
this.descriptionEl = this.hostEl.querySelector('[part="description"]');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unofficialbox/box-open-elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|