@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.
Files changed (55) hide show
  1. package/dist/components/feedback/alert.js +1 -16
  2. package/dist/components/feedback/path.d.ts +84 -0
  3. package/dist/components/feedback/path.js +455 -0
  4. package/dist/components/feedback/skeleton.d.ts +70 -0
  5. package/dist/components/feedback/skeleton.js +231 -1
  6. package/dist/components/feedback/toast.d.ts +38 -0
  7. package/dist/components/feedback/toast.js +189 -13
  8. package/dist/components/feedback/tone.d.ts +16 -0
  9. package/dist/components/feedback/tone.js +31 -0
  10. package/dist/components/layout/grid.d.ts +41 -0
  11. package/dist/components/layout/grid.js +152 -0
  12. package/dist/components/overlays/command-palette.d.ts +1 -0
  13. package/dist/components/overlays/command-palette.js +42 -3
  14. package/dist/components/overlays/context-menu.js +11 -1
  15. package/dist/components/overlays/dialog.d.ts +1 -0
  16. package/dist/components/overlays/dialog.js +43 -3
  17. package/dist/components/overlays/drawer.d.ts +1 -3
  18. package/dist/components/overlays/drawer.js +4 -20
  19. package/dist/components/overlays/guide-tooltip.js +15 -1
  20. package/dist/components/overlays/popover.js +16 -1
  21. package/dist/components/overlays/shortcuts-overlay.d.ts +1 -0
  22. package/dist/components/overlays/shortcuts-overlay.js +40 -3
  23. package/dist/components/overlays/tooltip.js +15 -1
  24. package/dist/element-maps.d.ts +5 -3
  25. package/dist/entries/grid.d.ts +1 -0
  26. package/dist/entries/grid.js +1 -0
  27. package/dist/entries/path.d.ts +1 -0
  28. package/dist/entries/path.js +1 -0
  29. package/dist/foundations/layout/grid.d.ts +70 -0
  30. package/dist/foundations/layout/grid.js +84 -0
  31. package/dist/foundations/layout/index.d.ts +1 -0
  32. package/dist/foundations/layout/index.js +3 -0
  33. package/dist/foundations/overlay/index.d.ts +1 -0
  34. package/dist/foundations/overlay/index.js +1 -0
  35. package/dist/foundations/overlay/top-layer.d.ts +62 -0
  36. package/dist/foundations/overlay/top-layer.js +95 -0
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.js +5 -1
  39. package/dist/patterns/audit/audit-log.js +0 -1
  40. package/dist/patterns/lineage/lineage-graph.d.ts +7 -0
  41. package/dist/patterns/lineage/lineage-graph.js +67 -6
  42. package/dist/patterns/lineage/provenance-strip.js +106 -15
  43. package/dist/patterns/notifications/notification-inbox.js +2 -18
  44. package/dist/patterns/timeline/timeline.js +44 -12
  45. package/dist/patterns/versions/graph-arrows.d.ts +22 -0
  46. package/dist/patterns/versions/graph-arrows.js +13 -0
  47. package/dist/patterns/versions/index.d.ts +1 -0
  48. package/dist/patterns/versions/index.js +1 -0
  49. package/dist/patterns/versions/version-graph.d.ts +7 -0
  50. package/dist/patterns/versions/version-graph.js +54 -6
  51. package/package.json +1 -1
  52. package/dist/components/feedback/stage-path.d.ts +0 -44
  53. package/dist/components/feedback/stage-path.js +0 -235
  54. package/dist/entries/stage-path.d.ts +0 -1
  55. 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();
@@ -16,6 +16,7 @@ export declare class CommandPalette extends BaseElement {
16
16
  static readonly tagName: string;
17
17
  static get observedAttributes(): string[];
18
18
  private hostEl;
19
+ private backdropEl;
19
20
  private readonly focusRestore;
20
21
  private openValue;
21
22
  private wasOpen;
@@ -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
- <div part="backdrop">
490
- <div part="palette" role="dialog" aria-modal="true" aria-label="Command 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
- </div>
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();
@@ -9,6 +9,7 @@ export declare class Dialog extends BaseElement {
9
9
  private hostEl;
10
10
  private titleEl;
11
11
  private descriptionEl;
12
+ private backdropEl;
12
13
  private confirmEl;
13
14
  private readonly focusRestore;
14
15
  get open(): boolean;
@@ -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
- <div part="backdrop">
265
- <section part="dialog" role="dialog" aria-modal="true" tabindex="-1" aria-labelledby="dialog-title" data-size="${this.size}">
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
- </div>
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
- * 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.
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
- * 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.
293
+ * The guards live in the shared foundation, which every overlay now uses.
295
294
  */
296
295
  promoteTopLayer() {
297
- const dialog = this.backdropEl;
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
- const dialog = this.backdropEl;
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();
@@ -16,6 +16,7 @@ export declare class ShortcutsOverlay extends BaseElement {
16
16
  static readonly tagName: string;
17
17
  static get observedAttributes(): string[];
18
18
  private hostEl;
19
+ private backdropEl;
19
20
  private readonly focusRestore;
20
21
  private wasOpen;
21
22
  private commandsRaw;