@unofficialbox/box-open-elements 0.9.0 → 0.11.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/feedback/alert.js +1 -16
- package/dist/components/feedback/path.d.ts +84 -0
- package/dist/components/feedback/path.js +455 -0
- package/dist/components/feedback/skeleton.d.ts +70 -0
- package/dist/components/feedback/skeleton.js +231 -1
- package/dist/components/feedback/toast.d.ts +38 -0
- package/dist/components/feedback/toast.js +189 -13
- package/dist/components/feedback/tone.d.ts +16 -0
- package/dist/components/feedback/tone.js +31 -0
- package/dist/components/layout/grid.d.ts +41 -0
- package/dist/components/layout/grid.js +152 -0
- 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 +1 -3
- package/dist/components/overlays/drawer.js +4 -20
- 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/element-maps.d.ts +5 -3
- package/dist/entries/grid.d.ts +1 -0
- package/dist/entries/grid.js +1 -0
- package/dist/entries/path.d.ts +1 -0
- package/dist/entries/path.js +1 -0
- package/dist/foundations/layout/grid.d.ts +70 -0
- package/dist/foundations/layout/grid.js +84 -0
- package/dist/foundations/layout/index.d.ts +1 -0
- package/dist/foundations/layout/index.js +3 -0
- 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/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/patterns/audit/audit-log.js +0 -1
- package/dist/patterns/lineage/lineage-graph.d.ts +7 -0
- package/dist/patterns/lineage/lineage-graph.js +67 -6
- package/dist/patterns/lineage/provenance-strip.js +106 -15
- package/dist/patterns/notifications/notification-inbox.js +2 -18
- package/dist/patterns/timeline/timeline.js +44 -12
- package/dist/patterns/versions/graph-arrows.d.ts +22 -0
- package/dist/patterns/versions/graph-arrows.js +13 -0
- package/dist/patterns/versions/index.d.ts +1 -0
- package/dist/patterns/versions/index.js +1 -0
- package/dist/patterns/versions/version-graph.d.ts +7 -0
- package/dist/patterns/versions/version-graph.js +54 -6
- package/package.json +1 -1
- package/dist/components/feedback/stage-path.d.ts +0 -44
- package/dist/components/feedback/stage-path.js +0 -235
- package/dist/entries/stage-path.d.ts +0 -1
- package/dist/entries/stage-path.js +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status vocabulary shared by the feedback surfaces.
|
|
3
|
+
*
|
|
4
|
+
* `box-alert` and `box-toast` both state their tone in words for screen
|
|
5
|
+
* readers, and both must say the *same* words: a reader who hears "In progress"
|
|
6
|
+
* from an alert should not hear "Inprogress" from a toast reporting the same
|
|
7
|
+
* thing. One definition is what keeps them in step.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The tone as a word, for assistive technology.
|
|
11
|
+
*
|
|
12
|
+
* Tone is otherwise carried by fill and glyph, neither of which reaches a
|
|
13
|
+
* screen reader. An unrecognised tone is title-cased rather than dropped —
|
|
14
|
+
* a host that invents a tone should still get something spoken.
|
|
15
|
+
*/
|
|
16
|
+
export const toneAccessibleLabel = (tone) => {
|
|
17
|
+
switch (tone) {
|
|
18
|
+
case "success":
|
|
19
|
+
return "Success";
|
|
20
|
+
case "error":
|
|
21
|
+
return "Error";
|
|
22
|
+
case "warning":
|
|
23
|
+
return "Warning";
|
|
24
|
+
case "inprogress":
|
|
25
|
+
return "In progress";
|
|
26
|
+
case "info":
|
|
27
|
+
return "Info";
|
|
28
|
+
default:
|
|
29
|
+
return tone.charAt(0).toUpperCase() + tone.slice(1);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adobe Spectrum's responsive grid, for laying out arbitrary content.
|
|
4
|
+
*
|
|
5
|
+
* Twelve columns by default, with gutters stepping 16 → 24 → 32 → 40 → 48px
|
|
6
|
+
* across Spectrum's breakpoints. Children declare their own placement:
|
|
7
|
+
*
|
|
8
|
+
* ```html
|
|
9
|
+
* <box-grid columns="12">
|
|
10
|
+
* <article data-span="8">Main</article>
|
|
11
|
+
* <aside data-span="4">Sidebar</aside>
|
|
12
|
+
* <footer data-span="6" data-offset="3">Centred</footer>
|
|
13
|
+
* </box-grid>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Placement lives on the children rather than in a payload on the host,
|
|
17
|
+
* because the children are arbitrary content the author already writes — an
|
|
18
|
+
* `items` array would have to address them positionally and would silently
|
|
19
|
+
* mis-place everything the moment one was inserted.
|
|
20
|
+
*
|
|
21
|
+
* `data-offset` sets an explicit `grid-column-start`. That is the author's
|
|
22
|
+
* stated intent here, unlike `box-skeleton`, which generates its own regions
|
|
23
|
+
* and uses spacers so auto-placement is not disturbed.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Grid extends BaseElement {
|
|
26
|
+
static readonly tagName: string;
|
|
27
|
+
static get observedAttributes(): string[];
|
|
28
|
+
private gridEl;
|
|
29
|
+
/** Columns in the grid. Defaults to Spectrum's twelve. */
|
|
30
|
+
get columns(): number;
|
|
31
|
+
set columns(value: number);
|
|
32
|
+
/**
|
|
33
|
+
* Height of one row. Unset by default: a layout grid's rows should be as
|
|
34
|
+
* tall as their content, and pinning them would crop it.
|
|
35
|
+
*/
|
|
36
|
+
get rowHeight(): string;
|
|
37
|
+
set rowHeight(value: string);
|
|
38
|
+
protected renderTemplate(): void;
|
|
39
|
+
protected setupListeners(): void;
|
|
40
|
+
protected update(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { BOE_GRID_COLUMNS, BOE_GRID_GUTTER_PROPERTY, boeGridGutterStyles, resolveGridCount, } from "../../foundations/layout/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-grid";
|
|
4
|
+
/**
|
|
5
|
+
* How far a child's placement attributes are honoured.
|
|
6
|
+
*
|
|
7
|
+
* Placement is expressed in CSS rules generated per value, not computed per
|
|
8
|
+
* element, so the ceiling is a real limit rather than a guess. Twenty-four
|
|
9
|
+
* covers a twelve-column grid used at double density without emitting rules
|
|
10
|
+
* nobody will match.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_TRACKS = 24;
|
|
13
|
+
/**
|
|
14
|
+
* Placement rules, one per value.
|
|
15
|
+
*
|
|
16
|
+
* A slotted child cannot be given a computed `grid-column` without writing to
|
|
17
|
+
* the light DOM, which is the host's tree, not ours. Generating a rule per
|
|
18
|
+
* value keeps placement declarative and leaves the author's markup untouched.
|
|
19
|
+
*
|
|
20
|
+
* `grid-column-start` and `grid-column-end` are set by separate rules on
|
|
21
|
+
* purpose: they compose, so offset and span combine without needing a rule for
|
|
22
|
+
* every offset × span pair.
|
|
23
|
+
*/
|
|
24
|
+
const placementRules = () => {
|
|
25
|
+
const rules = [];
|
|
26
|
+
for (let value = 1; value <= MAX_TRACKS; value += 1) {
|
|
27
|
+
rules.push(` ::slotted([data-span="${value}"]) { grid-column-end: span ${value}; }`);
|
|
28
|
+
rules.push(` ::slotted([data-row-span="${value}"]) { grid-row: span ${value}; }`);
|
|
29
|
+
}
|
|
30
|
+
for (let value = 1; value < MAX_TRACKS; value += 1) {
|
|
31
|
+
// An offset of N starts the region at column N + 1.
|
|
32
|
+
rules.push(` ::slotted([data-offset="${value}"]) { grid-column-start: ${value + 1}; }`);
|
|
33
|
+
}
|
|
34
|
+
return rules.join("\n");
|
|
35
|
+
};
|
|
36
|
+
const elementStyles = `
|
|
37
|
+
:host {
|
|
38
|
+
display: block;
|
|
39
|
+
color: inherit;
|
|
40
|
+
font: inherit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:host([hidden]) {
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
${boeGridGutterStyles(":host")}
|
|
48
|
+
|
|
49
|
+
[part="grid"] {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: repeat(var(--boe-grid-columns, ${BOE_GRID_COLUMNS}), minmax(0, 1fr));
|
|
52
|
+
gap: var(${BOE_GRID_GUTTER_PROPERTY});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Rows are only constrained when the author asks for them; otherwise each
|
|
56
|
+
row is as tall as its content, which is what a layout grid should do.
|
|
57
|
+
Spectrum's layout regions have no intrinsic height either — at minimum
|
|
58
|
+
they are as tall as the components inside them. */
|
|
59
|
+
:host([row-height]) [part="grid"] {
|
|
60
|
+
grid-auto-rows: var(--boe-grid-row-height);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Without this the slot itself is the grid item and every child lands in one
|
|
64
|
+
cell. display: contents dissolves it, so the assigned children become the
|
|
65
|
+
grid's own items. */
|
|
66
|
+
slot {
|
|
67
|
+
display: contents;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* A region with no placement attributes occupies one column, so a bare list
|
|
71
|
+
of children reads as an even row. */
|
|
72
|
+
::slotted(*) {
|
|
73
|
+
grid-column-end: span 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
${placementRules()}
|
|
77
|
+
`;
|
|
78
|
+
/**
|
|
79
|
+
* Adobe Spectrum's responsive grid, for laying out arbitrary content.
|
|
80
|
+
*
|
|
81
|
+
* Twelve columns by default, with gutters stepping 16 → 24 → 32 → 40 → 48px
|
|
82
|
+
* across Spectrum's breakpoints. Children declare their own placement:
|
|
83
|
+
*
|
|
84
|
+
* ```html
|
|
85
|
+
* <box-grid columns="12">
|
|
86
|
+
* <article data-span="8">Main</article>
|
|
87
|
+
* <aside data-span="4">Sidebar</aside>
|
|
88
|
+
* <footer data-span="6" data-offset="3">Centred</footer>
|
|
89
|
+
* </box-grid>
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* Placement lives on the children rather than in a payload on the host,
|
|
93
|
+
* because the children are arbitrary content the author already writes — an
|
|
94
|
+
* `items` array would have to address them positionally and would silently
|
|
95
|
+
* mis-place everything the moment one was inserted.
|
|
96
|
+
*
|
|
97
|
+
* `data-offset` sets an explicit `grid-column-start`. That is the author's
|
|
98
|
+
* stated intent here, unlike `box-skeleton`, which generates its own regions
|
|
99
|
+
* and uses spacers so auto-placement is not disturbed.
|
|
100
|
+
*/
|
|
101
|
+
export class Grid extends BaseElement {
|
|
102
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
103
|
+
static get observedAttributes() {
|
|
104
|
+
return ["columns", "row-height"];
|
|
105
|
+
}
|
|
106
|
+
gridEl;
|
|
107
|
+
/** Columns in the grid. Defaults to Spectrum's twelve. */
|
|
108
|
+
get columns() {
|
|
109
|
+
return resolveGridCount(this.getAttribute("columns"), BOE_GRID_COLUMNS);
|
|
110
|
+
}
|
|
111
|
+
set columns(value) {
|
|
112
|
+
this.setAttribute("columns", String(value));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Height of one row. Unset by default: a layout grid's rows should be as
|
|
116
|
+
* tall as their content, and pinning them would crop it.
|
|
117
|
+
*/
|
|
118
|
+
get rowHeight() {
|
|
119
|
+
return this.getAttribute("row-height") ?? "";
|
|
120
|
+
}
|
|
121
|
+
set rowHeight(value) {
|
|
122
|
+
if (value) {
|
|
123
|
+
this.setAttribute("row-height", value);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.removeAttribute("row-height");
|
|
127
|
+
}
|
|
128
|
+
renderTemplate() {
|
|
129
|
+
if (!this.shadowRoot) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.shadowRoot.innerHTML = `<style>${elementStyles}</style><div part="grid"><slot></slot></div>`;
|
|
133
|
+
this.gridEl = this.shadowRoot.querySelector('[part="grid"]');
|
|
134
|
+
}
|
|
135
|
+
setupListeners() {
|
|
136
|
+
// Layout only: placement is declared by the children and applied in CSS.
|
|
137
|
+
}
|
|
138
|
+
update() {
|
|
139
|
+
if (!this.gridEl) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.gridEl.style.setProperty("--boe-grid-columns", String(this.columns));
|
|
143
|
+
// Via the CSSOM rather than interpolated into markup. Note that custom
|
|
144
|
+
// properties are *not* validated the way a standard property like `width`
|
|
145
|
+
// is — they accept almost any token sequence — so this does not sanitise
|
|
146
|
+
// the value. What makes it safe is that the value never reaches an HTML
|
|
147
|
+
// string: a nonsensical one makes `grid-auto-rows` invalid at computed-value
|
|
148
|
+
// time, which falls back to `auto`, and rows simply size to their content.
|
|
149
|
+
this.gridEl.style.setProperty("--boe-grid-row-height", this.rowHeight);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
Grid.register();
|
|
@@ -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"]');
|
|
@@ -43,9 +43,7 @@ export declare class Drawer extends BaseElement {
|
|
|
43
43
|
* `NotFoundError`, because the node it tried to remove was no longer its
|
|
44
44
|
* child.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
* `showPopover`, so the unit suite exercises everything except the promotion
|
|
48
|
-
* itself, and that is verified in a browser.
|
|
46
|
+
* The guards live in the shared foundation, which every overlay now uses.
|
|
49
47
|
*/
|
|
50
48
|
private promoteTopLayer;
|
|
51
49
|
private dismissTopLayer;
|
|
@@ -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";
|
|
@@ -289,30 +290,13 @@ export class Drawer extends BaseElement {
|
|
|
289
290
|
* `NotFoundError`, because the node it tried to remove was no longer its
|
|
290
291
|
* child.
|
|
291
292
|
*
|
|
292
|
-
*
|
|
293
|
-
* `showPopover`, so the unit suite exercises everything except the promotion
|
|
294
|
-
* itself, and that is verified in a browser.
|
|
293
|
+
* The guards live in the shared foundation, which every overlay now uses.
|
|
295
294
|
*/
|
|
296
295
|
promoteTopLayer() {
|
|
297
|
-
|
|
298
|
-
if (!dialog || typeof dialog.showModal !== "function" || dialog.open) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
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.
|
|
308
|
-
}
|
|
296
|
+
promoteModal(this.backdropEl);
|
|
309
297
|
}
|
|
310
298
|
dismissTopLayer() {
|
|
311
|
-
|
|
312
|
-
if (!dialog || typeof dialog.close !== "function" || !dialog.open) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
dialog.close();
|
|
299
|
+
dismissModal(this.backdropEl);
|
|
316
300
|
}
|
|
317
301
|
renderTemplate() {
|
|
318
302
|
if (!this.shadowRoot) {
|
|
@@ -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();
|