@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
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { boeRadius } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { BOE_GRID_COLUMNS, BOE_GRID_GUTTER_PROPERTY, boeGridGutterStyles, isBoeGridPlacementRecord, resolveBoeGridPlacement, resolveGridCount, } from "../../foundations/layout/index.js";
|
|
3
4
|
import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
|
|
4
5
|
const DEFAULT_TAG_NAME = "box-skeleton";
|
|
6
|
+
const SKELETON_VARIANTS = new Set(["box", "line", "grid"]);
|
|
7
|
+
/**
|
|
8
|
+
* Narrow an author-supplied variant, falling back to `box`.
|
|
9
|
+
*
|
|
10
|
+
* A typo should render the original single rectangle rather than an empty
|
|
11
|
+
* element: every multi-part branch is scoped to a known value.
|
|
12
|
+
*/
|
|
13
|
+
export const resolveSkeletonVariant = (value) => SKELETON_VARIANTS.has(value) ? value : "box";
|
|
14
|
+
/**
|
|
15
|
+
* The columns in Adobe Spectrum's responsive grid.
|
|
16
|
+
*
|
|
17
|
+
* Delegated to the layout foundation that `box-grid` also builds on: a
|
|
18
|
+
* skeleton stands in for a real layout, so if the two disagreed about what a
|
|
19
|
+
* column is worth the placeholder would stop matching what replaces it.
|
|
20
|
+
*/
|
|
21
|
+
export const SKELETON_DEFAULT_COLUMNS = BOE_GRID_COLUMNS;
|
|
22
|
+
/** Attribute payloads are author input — validate every record. */
|
|
23
|
+
export const isSkeletonGridItemRecord = isBoeGridPlacementRecord;
|
|
24
|
+
/**
|
|
25
|
+
* Fit a region to the grid, clamping every value to the declared totals.
|
|
26
|
+
*
|
|
27
|
+
* Still exported here because it is part of this component's published
|
|
28
|
+
* surface; the behaviour now lives in the foundation.
|
|
29
|
+
*/
|
|
30
|
+
export const resolveSkeletonGridItem = resolveBoeGridPlacement;
|
|
5
31
|
const skeletonStyles = `
|
|
6
32
|
:host {
|
|
7
33
|
display: inline-block;
|
|
8
34
|
color: inherit;
|
|
9
35
|
font: inherit;
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
${boeGridGutterStyles(":host")}
|
|
40
|
+
|
|
41
|
+
/* The multi-part variants are layout, so they take the full width they are
|
|
42
|
+
given rather than shrink-wrapping like the single box. */
|
|
43
|
+
:host([variant="line"]),
|
|
44
|
+
:host([variant="grid"]) {
|
|
45
|
+
display: block;
|
|
10
46
|
}
|
|
11
47
|
|
|
12
48
|
[part="skeleton"] {
|
|
@@ -22,6 +58,38 @@ const skeletonStyles = `
|
|
|
22
58
|
animation: boe-skeleton-shimmer ${boeMotionDuration.shimmer} ${boeMotionEasing.standard} infinite;
|
|
23
59
|
}
|
|
24
60
|
|
|
61
|
+
[part="lines"] {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 0.55rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[part="lines"] [part="skeleton"] {
|
|
68
|
+
display: block;
|
|
69
|
+
inline-size: 100%;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* The last line stops short, the way a paragraph's does. Without it a stack
|
|
73
|
+
of equal bars reads as a table rather than as text. */
|
|
74
|
+
[part="lines"] [part="skeleton"]:last-child:not(:only-child) {
|
|
75
|
+
inline-size: 62%;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[part="grid"] {
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: repeat(var(--boe-skeleton-columns, ${BOE_GRID_COLUMNS}), minmax(0, 1fr));
|
|
81
|
+
grid-auto-rows: var(--boe-skeleton-row-height, 48px);
|
|
82
|
+
gap: var(${BOE_GRID_GUTTER_PROPERTY});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Offsets are spacers rather than an explicit grid-column-start, so they
|
|
86
|
+
compose with auto-placement instead of fighting it: an explicit start makes
|
|
87
|
+
the item hunt for a row where that exact column is free, which reorders the
|
|
88
|
+
regions the author listed. */
|
|
89
|
+
[part="grid-offset"] {
|
|
90
|
+
visibility: hidden;
|
|
91
|
+
}
|
|
92
|
+
|
|
25
93
|
@keyframes boe-skeleton-shimmer {
|
|
26
94
|
0% {
|
|
27
95
|
background-position: 100% 0;
|
|
@@ -37,11 +105,15 @@ const skeletonStyles = `
|
|
|
37
105
|
export class Skeleton extends BaseElement {
|
|
38
106
|
static tagName = DEFAULT_TAG_NAME;
|
|
39
107
|
static get observedAttributes() {
|
|
40
|
-
return ["height", "width"];
|
|
108
|
+
return ["columns", "height", "items", "lines", "row-height", "rows", "variant", "width"];
|
|
41
109
|
}
|
|
42
110
|
skeletonEl;
|
|
111
|
+
rootEl;
|
|
43
112
|
appliedWidth = "";
|
|
44
113
|
appliedHeight = "";
|
|
114
|
+
renderedVariant = null;
|
|
115
|
+
itemsRaw = null;
|
|
116
|
+
itemsCache = [];
|
|
45
117
|
get width() {
|
|
46
118
|
return this.getAttribute("width") ?? "100%";
|
|
47
119
|
}
|
|
@@ -54,6 +126,85 @@ export class Skeleton extends BaseElement {
|
|
|
54
126
|
set height(value) {
|
|
55
127
|
this.setAttribute("height", value);
|
|
56
128
|
}
|
|
129
|
+
/** Shape of the placeholder. An unknown value falls back to `box`. */
|
|
130
|
+
get variant() {
|
|
131
|
+
return resolveSkeletonVariant(this.getAttribute("variant"));
|
|
132
|
+
}
|
|
133
|
+
set variant(value) {
|
|
134
|
+
this.setAttribute("variant", value);
|
|
135
|
+
}
|
|
136
|
+
/** Lines drawn by the `line` variant. Values below 1 render a single line. */
|
|
137
|
+
get lines() {
|
|
138
|
+
const parsed = Number.parseInt(this.getAttribute("lines") ?? "", 10);
|
|
139
|
+
return Number.isFinite(parsed) ? Math.max(parsed, 1) : 3;
|
|
140
|
+
}
|
|
141
|
+
set lines(value) {
|
|
142
|
+
this.setAttribute("lines", String(value));
|
|
143
|
+
}
|
|
144
|
+
/** Columns in the `grid` variant. Defaults to Spectrum's twelve. */
|
|
145
|
+
get columns() {
|
|
146
|
+
return resolveGridCount(this.getAttribute("columns"), BOE_GRID_COLUMNS);
|
|
147
|
+
}
|
|
148
|
+
set columns(value) {
|
|
149
|
+
this.setAttribute("columns", String(value));
|
|
150
|
+
}
|
|
151
|
+
/** Rows in the `grid` variant. Also the ceiling for any region's `rowSpan`. */
|
|
152
|
+
get rows() {
|
|
153
|
+
return resolveGridCount(this.getAttribute("rows"), 1);
|
|
154
|
+
}
|
|
155
|
+
set rows(value) {
|
|
156
|
+
this.setAttribute("rows", String(value));
|
|
157
|
+
}
|
|
158
|
+
/** Height of one grid row; a region spanning two is twice this plus a gutter. */
|
|
159
|
+
get rowHeight() {
|
|
160
|
+
return this.getAttribute("row-height") ?? "48px";
|
|
161
|
+
}
|
|
162
|
+
set rowHeight(value) {
|
|
163
|
+
this.setAttribute("row-height", value);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Regions of the `grid` variant, in order.
|
|
167
|
+
*
|
|
168
|
+
* With none given the grid is filled with single-column regions — `rows` ×
|
|
169
|
+
* `columns` of them — so `rows` and `columns` alone describe a uniform grid.
|
|
170
|
+
*/
|
|
171
|
+
get items() {
|
|
172
|
+
const raw = this.getAttribute("items");
|
|
173
|
+
if (!raw) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
if (raw !== this.itemsRaw) {
|
|
177
|
+
this.itemsRaw = raw;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(raw);
|
|
180
|
+
this.itemsCache =
|
|
181
|
+
Array.isArray(parsed) && parsed.every(isSkeletonGridItemRecord)
|
|
182
|
+
? parsed
|
|
183
|
+
: [];
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
this.itemsCache = [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...this.itemsCache];
|
|
190
|
+
}
|
|
191
|
+
set items(value) {
|
|
192
|
+
if (value.length) {
|
|
193
|
+
this.setAttribute("items", JSON.stringify(value));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.removeAttribute("items");
|
|
197
|
+
}
|
|
198
|
+
/** The grid's regions as rendered: clamped to `columns` and `rows`. */
|
|
199
|
+
get resolvedItems() {
|
|
200
|
+
const columns = this.columns;
|
|
201
|
+
const rows = this.rows;
|
|
202
|
+
const declared = this.items;
|
|
203
|
+
const source = declared.length
|
|
204
|
+
? declared
|
|
205
|
+
: Array.from({ length: rows * columns }, () => ({}));
|
|
206
|
+
return source.map(item => resolveSkeletonGridItem(item, columns, rows));
|
|
207
|
+
}
|
|
57
208
|
renderTemplate() {
|
|
58
209
|
if (!this.shadowRoot) {
|
|
59
210
|
return;
|
|
@@ -65,8 +216,29 @@ export class Skeleton extends BaseElement {
|
|
|
65
216
|
this.skeletonEl = this.shadowRoot.querySelector('[part="skeleton"]');
|
|
66
217
|
this.appliedWidth = "";
|
|
67
218
|
this.appliedHeight = "";
|
|
219
|
+
this.renderedVariant = "box";
|
|
68
220
|
}
|
|
69
221
|
update() {
|
|
222
|
+
if (!this.shadowRoot) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const variant = this.variant;
|
|
226
|
+
if (variant !== "box") {
|
|
227
|
+
this.updateGroup(variant);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (this.renderedVariant !== "box") {
|
|
231
|
+
// Coming back from a multi-part variant: restore the single box that the
|
|
232
|
+
// width/height path writes into.
|
|
233
|
+
this.shadowRoot.innerHTML = `
|
|
234
|
+
<style>${skeletonStyles}</style>
|
|
235
|
+
<span part="skeleton" style="display:inline-block;" aria-hidden="true"></span>
|
|
236
|
+
`;
|
|
237
|
+
this.skeletonEl = this.shadowRoot.querySelector('[part="skeleton"]');
|
|
238
|
+
this.appliedWidth = "";
|
|
239
|
+
this.appliedHeight = "";
|
|
240
|
+
this.renderedVariant = "box";
|
|
241
|
+
}
|
|
70
242
|
if (!this.skeletonEl) {
|
|
71
243
|
return;
|
|
72
244
|
}
|
|
@@ -85,5 +257,63 @@ export class Skeleton extends BaseElement {
|
|
|
85
257
|
this.appliedHeight = nextHeight;
|
|
86
258
|
}
|
|
87
259
|
}
|
|
260
|
+
/** Render the `line` or `grid` variant, which draw many bars rather than one. */
|
|
261
|
+
updateGroup(variant) {
|
|
262
|
+
const shadowRoot = this.shadowRoot;
|
|
263
|
+
if (this.renderedVariant !== variant) {
|
|
264
|
+
shadowRoot.innerHTML = `
|
|
265
|
+
<style>${skeletonStyles}</style>
|
|
266
|
+
<div part="${variant === "line" ? "lines" : "grid"}" aria-hidden="true"></div>
|
|
267
|
+
`;
|
|
268
|
+
this.rootEl = shadowRoot.querySelector(`[part="${variant === "line" ? "lines" : "grid"}"]`);
|
|
269
|
+
this.renderedVariant = variant;
|
|
270
|
+
this.appliedWidth = "";
|
|
271
|
+
this.appliedHeight = "";
|
|
272
|
+
}
|
|
273
|
+
if (variant === "line") {
|
|
274
|
+
this.renderLines();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.renderGrid();
|
|
278
|
+
}
|
|
279
|
+
renderLines() {
|
|
280
|
+
const count = this.lines;
|
|
281
|
+
const height = this.height;
|
|
282
|
+
// Reuse the bars already there: only the count changes between renders, and
|
|
283
|
+
// replacing every node would restart the shimmer on each update.
|
|
284
|
+
while (this.rootEl.childElementCount > count) {
|
|
285
|
+
this.rootEl.lastElementChild.remove();
|
|
286
|
+
}
|
|
287
|
+
while (this.rootEl.childElementCount < count) {
|
|
288
|
+
const line = document.createElement("span");
|
|
289
|
+
line.setAttribute("part", "skeleton");
|
|
290
|
+
this.rootEl.append(line);
|
|
291
|
+
}
|
|
292
|
+
for (const child of Array.from(this.rootEl.children)) {
|
|
293
|
+
// Via the CSSOM, for the same reason the box variant does: the value is
|
|
294
|
+
// author input and setProperty drops anything that is not a length.
|
|
295
|
+
child.style.setProperty("height", height);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
renderGrid() {
|
|
299
|
+
this.rootEl.style.setProperty("--boe-skeleton-columns", String(this.columns));
|
|
300
|
+
this.rootEl.style.setProperty("--boe-skeleton-row-height", this.rowHeight);
|
|
301
|
+
// Rebuilt rather than patched: a change to items can alter every region's
|
|
302
|
+
// span and offset, so there is no stable identity to reuse.
|
|
303
|
+
this.rootEl.replaceChildren();
|
|
304
|
+
for (const item of this.resolvedItems) {
|
|
305
|
+
if (item.offset > 0) {
|
|
306
|
+
const spacer = document.createElement("span");
|
|
307
|
+
spacer.setAttribute("part", "grid-offset");
|
|
308
|
+
spacer.style.setProperty("grid-column", `span ${item.offset}`);
|
|
309
|
+
this.rootEl.append(spacer);
|
|
310
|
+
}
|
|
311
|
+
const region = document.createElement("span");
|
|
312
|
+
region.setAttribute("part", "skeleton");
|
|
313
|
+
region.style.setProperty("grid-column", `span ${item.span}`);
|
|
314
|
+
region.style.setProperty("grid-row", `span ${item.rowSpan}`);
|
|
315
|
+
this.rootEl.append(region);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
88
318
|
}
|
|
89
319
|
Skeleton.register();
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the toast clears itself.
|
|
4
|
+
*
|
|
5
|
+
* `dismissible` auto-dismisses after `duration` and can also be closed by hand;
|
|
6
|
+
* `sticky` stays until the reader closes it, whatever `duration` says. Both
|
|
7
|
+
* carry the close control — a toast the reader cannot get rid of is a trap.
|
|
8
|
+
*/
|
|
9
|
+
export type ToastMode = "dismissible" | "sticky";
|
|
10
|
+
/** Narrow an author-supplied mode, falling back to `dismissible`. */
|
|
11
|
+
export declare const resolveToastMode: (value: string | null | undefined) => ToastMode;
|
|
2
12
|
export declare class Toast extends BaseElement {
|
|
3
13
|
static readonly tagName: string;
|
|
4
14
|
static get observedAttributes(): string[];
|
|
5
15
|
private openValue;
|
|
6
16
|
private timeoutId;
|
|
7
17
|
private toastEl;
|
|
18
|
+
private iconEl;
|
|
19
|
+
private headingEl;
|
|
20
|
+
private toneLabelEl;
|
|
8
21
|
private messageEl;
|
|
22
|
+
private renderedTone;
|
|
9
23
|
private actionSlot;
|
|
10
24
|
private dismissEl;
|
|
11
25
|
/** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
|
|
@@ -16,6 +30,30 @@ export declare class Toast extends BaseElement {
|
|
|
16
30
|
private scheduleAutoDismiss;
|
|
17
31
|
get message(): string;
|
|
18
32
|
set message(value: string);
|
|
33
|
+
/**
|
|
34
|
+
* Optional bold line above the message — "Upload failed" over "3 of 12 files
|
|
35
|
+
* could not be read". Named to match `box-alert`, which established the
|
|
36
|
+
* heading-plus-message shape in this family. Salesforce calls it `label`.
|
|
37
|
+
*/
|
|
38
|
+
get heading(): string;
|
|
39
|
+
set heading(value: string);
|
|
40
|
+
/**
|
|
41
|
+
* Drop the outline for a softer, fill-only toast.
|
|
42
|
+
*
|
|
43
|
+
* Purely presentational and handled entirely in CSS, so it is deliberately
|
|
44
|
+
* not an observed attribute — there is nothing to re-render. The default
|
|
45
|
+
* stays bordered: that outline tracks box-ui-elements' `.notification` and
|
|
46
|
+
* is pinned by the colour conformance manifest.
|
|
47
|
+
*/
|
|
48
|
+
get borderless(): boolean;
|
|
49
|
+
set borderless(value: boolean);
|
|
50
|
+
/**
|
|
51
|
+
* Whether the toast clears itself. `sticky` overrides any `duration`: a
|
|
52
|
+
* caller that has said "this one stays" should not have it taken away by a
|
|
53
|
+
* duration set elsewhere.
|
|
54
|
+
*/
|
|
55
|
+
get mode(): ToastMode;
|
|
56
|
+
set mode(value: ToastMode);
|
|
19
57
|
get tone(): string;
|
|
20
58
|
set tone(value: string);
|
|
21
59
|
disconnectedCallback(): void;
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { toneAccessibleLabel } from "./tone.js";
|
|
2
3
|
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
4
|
import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
|
|
4
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
5
6
|
const DEFAULT_TAG_NAME = "box-toast";
|
|
7
|
+
const TOAST_MODES = new Set(["dismissible", "sticky"]);
|
|
8
|
+
/** Narrow an author-supplied mode, falling back to `dismissible`. */
|
|
9
|
+
export const resolveToastMode = (value) => TOAST_MODES.has(value) ? value : "dismissible";
|
|
10
|
+
/**
|
|
11
|
+
* Status glyphs, one per tone.
|
|
12
|
+
*
|
|
13
|
+
* A toast is read at a glance and often out of the corner of an eye, so the
|
|
14
|
+
* shape has to carry the meaning before the colour does — a round tick and a
|
|
15
|
+
* warning triangle are distinguishable to a reader who cannot separate green
|
|
16
|
+
* from amber. Literal markup, never author input, so innerHTML is safe here.
|
|
17
|
+
*/
|
|
18
|
+
const TONE_ICONS = {
|
|
19
|
+
info: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3.4a1.15 1.15 0 110 2.3 1.15 1.15 0 010-2.3zM11.1 15H8.9V9.3h2.2V15z"/></svg>`,
|
|
20
|
+
success: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm4.06 5.86-5 5.2a1 1 0 01-1.44 0L5.94 11.3A1 1 0 117.38 9.9l1.04 1.08 4.28-4.52a1 1 0 011.36 1.38z"/></svg>`,
|
|
21
|
+
warning: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M9.13 2.6 1.4 15.9a1 1 0 00.87 1.5h15.46a1 1 0 00.87-1.5L10.87 2.6a1 1 0 00-1.74 0zM11.1 15.4H8.9v-2.2h2.2v2.2zm0-3.4H8.9V7.6h2.2V12z"/></svg>`,
|
|
22
|
+
error: `<svg viewBox="0 0 20 20" fill="currentColor" focusable="false"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 2c1.29 0 2.48.41 3.46 1.1L5.1 13.46A6 6 0 0110 4zm0 12c-1.29 0-2.48-.41-3.46-1.1l8.36-8.36A6 6 0 0110 16z"/></svg>`,
|
|
23
|
+
};
|
|
24
|
+
/** The tone glyph, falling back to the info mark for an unknown tone. */
|
|
25
|
+
const toneIcon = (tone) => TONE_ICONS[tone] ?? TONE_ICONS.info;
|
|
26
|
+
const DISMISS_ICON = `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" focusable="false"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9"/></svg>`;
|
|
6
27
|
const toastStyles = `
|
|
7
28
|
:host {
|
|
8
29
|
display: block;
|
|
@@ -16,9 +37,18 @@ const toastStyles = `
|
|
|
16
37
|
display: none;
|
|
17
38
|
}
|
|
18
39
|
|
|
40
|
+
/* Fill, border, text colour and shadow all track box-ui-elements'
|
|
41
|
+
.notification and are pinned by the colour conformance manifest — they are
|
|
42
|
+
deliberately not derived from the tone accent. The --toast-accent property
|
|
43
|
+
exists only to colour the status glyph, which upstream has no equivalent
|
|
44
|
+
for, so adding it costs no conformance. */
|
|
19
45
|
[part="toast"] {
|
|
46
|
+
--toast-accent: var(--boe-token-text-text, #222222);
|
|
47
|
+
|
|
20
48
|
display: inline-flex;
|
|
21
|
-
|
|
49
|
+
/* Top, not centre: with a heading above the message the glyph belongs on
|
|
50
|
+
the heading's line, and centring floats it into the gap between them. */
|
|
51
|
+
align-items: flex-start;
|
|
22
52
|
gap: ${boeSpace[3]};
|
|
23
53
|
min-height: 48px;
|
|
24
54
|
max-inline-size: min(100%, 572px);
|
|
@@ -30,33 +60,107 @@ const toastStyles = `
|
|
|
30
60
|
box-shadow: 0 2px 6px rgb(0 0 0 / 15%);
|
|
31
61
|
}
|
|
32
62
|
|
|
63
|
+
/* Opt-in softer treatment: the outline goes, the geometry does not. Setting
|
|
64
|
+
the colour to transparent rather than the width to 0 keeps the 2px in the
|
|
65
|
+
box model, so a borderless toast is exactly the size of a bordered one and
|
|
66
|
+
the two can sit in the same stack without jumping. The background paints
|
|
67
|
+
under the border by default, so the fill simply extends into it.
|
|
68
|
+
|
|
69
|
+
The bordered default stays: the 2px solid text-colour rule below is pinned
|
|
70
|
+
by the colour conformance manifest against upstream's .notification, and
|
|
71
|
+
strict mode fails if that declaration goes. */
|
|
72
|
+
:host([borderless]) [part="toast"] {
|
|
73
|
+
border-color: transparent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* The neutral toast keeps upstream's grey fill and near-black border, so the
|
|
77
|
+
glyph is the only place the "this is information" signal can live. */
|
|
78
|
+
[part="toast"][data-tone="info"] {
|
|
79
|
+
--toast-accent: var(--boe-token-surface-surface-brand, #0061d5);
|
|
80
|
+
}
|
|
81
|
+
|
|
33
82
|
[part="toast"][data-tone="success"] {
|
|
83
|
+
--toast-accent: var(--boe-token-surface-status-surface-success, #26c281);
|
|
34
84
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 20%, #fff);
|
|
35
85
|
border-color: var(--boe-token-surface-status-surface-success, #26c281);
|
|
36
86
|
}
|
|
37
87
|
|
|
38
88
|
[part="toast"][data-tone="error"] {
|
|
89
|
+
--toast-accent: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
39
90
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 20%, #fff);
|
|
40
91
|
border-color: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
41
92
|
}
|
|
42
93
|
|
|
43
94
|
[part="toast"][data-tone="warning"],
|
|
44
95
|
[part="toast"][data-tone="inprogress"] {
|
|
96
|
+
--toast-accent: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
|
|
45
97
|
background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 20%, #fff);
|
|
46
98
|
border-color: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
|
|
47
99
|
}
|
|
48
100
|
|
|
49
|
-
|
|
101
|
+
.sr-only {
|
|
102
|
+
position: absolute;
|
|
103
|
+
inline-size: 1px;
|
|
104
|
+
block-size: 1px;
|
|
105
|
+
padding: 0;
|
|
106
|
+
margin: -1px;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
clip-path: inset(50%);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
border: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[part="icon"] {
|
|
114
|
+
flex: 0 0 auto;
|
|
115
|
+
inline-size: 20px;
|
|
116
|
+
block-size: 20px;
|
|
117
|
+
/* The accent at full strength: the glyph is the one element that should
|
|
118
|
+
read as the status colour rather than a darkened version of it. */
|
|
119
|
+
color: var(--toast-accent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[part="icon"] svg {
|
|
123
|
+
display: block;
|
|
124
|
+
inline-size: 100%;
|
|
125
|
+
block-size: 100%;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[part="content"] {
|
|
50
129
|
flex: 1 1 auto;
|
|
51
130
|
min-inline-size: 0;
|
|
52
|
-
padding-inline-end: ${boeSpace[
|
|
131
|
+
padding-inline-end: ${boeSpace[2]};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[part="heading"] {
|
|
135
|
+
display: block;
|
|
53
136
|
font-size: 15px;
|
|
54
137
|
font-weight: 700;
|
|
138
|
+
line-height: 1.3;
|
|
139
|
+
overflow-wrap: anywhere;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Optional: with no heading the message stands alone, and an empty element
|
|
143
|
+
would still contribute its line box. */
|
|
144
|
+
[part="heading"]:empty {
|
|
145
|
+
display: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
[part="message"] {
|
|
149
|
+
display: block;
|
|
150
|
+
font-size: 14px;
|
|
151
|
+
font-weight: 400;
|
|
55
152
|
line-height: 1.35;
|
|
56
153
|
/* Long unbroken tokens (URLs, filenames) wrap instead of overflowing. */
|
|
57
154
|
overflow-wrap: anywhere;
|
|
58
155
|
}
|
|
59
156
|
|
|
157
|
+
/* Without a heading the message is the toast's only line, so it carries the
|
|
158
|
+
weight the heading would have had. */
|
|
159
|
+
[part="heading"]:empty + [part="message"] {
|
|
160
|
+
font-size: 15px;
|
|
161
|
+
font-weight: 700;
|
|
162
|
+
}
|
|
163
|
+
|
|
60
164
|
/* Optional action affordance (e.g. "Undo") before the dismiss button.
|
|
61
165
|
Hidden — taking no flex gap — until content is assigned. */
|
|
62
166
|
[part="action"] {
|
|
@@ -77,21 +181,33 @@ const toastStyles = `
|
|
|
77
181
|
cursor: pointer;
|
|
78
182
|
}
|
|
79
183
|
|
|
184
|
+
/* An icon button rather than a "Dismiss" label: the toast is chrome over the
|
|
185
|
+
reader's work, and a word-width button competes with the message it sits
|
|
186
|
+
beside. The accessible name is on the button, so nothing is lost. */
|
|
80
187
|
[part="dismiss"] {
|
|
81
188
|
appearance: none;
|
|
82
189
|
flex: 0 0 auto;
|
|
83
|
-
|
|
190
|
+
display: inline-flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
inline-size: 24px;
|
|
194
|
+
block-size: 24px;
|
|
195
|
+
padding: 4px;
|
|
196
|
+
border: 0;
|
|
84
197
|
border-radius: ${boeRadius.med};
|
|
85
198
|
background: transparent;
|
|
86
|
-
color:
|
|
199
|
+
color: inherit;
|
|
87
200
|
font: inherit;
|
|
88
|
-
font-size: 13px;
|
|
89
|
-
font-weight: 700;
|
|
90
|
-
padding: 7px 13px;
|
|
91
201
|
cursor: pointer;
|
|
92
202
|
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard}, color ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
93
203
|
}
|
|
94
204
|
|
|
205
|
+
[part="dismiss"] svg {
|
|
206
|
+
display: block;
|
|
207
|
+
inline-size: 100%;
|
|
208
|
+
block-size: 100%;
|
|
209
|
+
}
|
|
210
|
+
|
|
95
211
|
[part="dismiss"]:hover:not(:disabled) {
|
|
96
212
|
background: rgb(0 0 0 / 6%);
|
|
97
213
|
}
|
|
@@ -111,12 +227,16 @@ const toastStyles = `
|
|
|
111
227
|
export class Toast extends BaseElement {
|
|
112
228
|
static tagName = DEFAULT_TAG_NAME;
|
|
113
229
|
static get observedAttributes() {
|
|
114
|
-
return ["message", "
|
|
230
|
+
return ["duration", "heading", "message", "mode", "open", "tone"];
|
|
115
231
|
}
|
|
116
232
|
openValue = false;
|
|
117
233
|
timeoutId = null;
|
|
118
234
|
toastEl;
|
|
235
|
+
iconEl;
|
|
236
|
+
headingEl;
|
|
237
|
+
toneLabelEl;
|
|
119
238
|
messageEl;
|
|
239
|
+
renderedTone = null;
|
|
120
240
|
actionSlot;
|
|
121
241
|
dismissEl;
|
|
122
242
|
/** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
|
|
@@ -161,7 +281,8 @@ export class Toast extends BaseElement {
|
|
|
161
281
|
clearTimeout(this.timeoutId);
|
|
162
282
|
this.timeoutId = null;
|
|
163
283
|
}
|
|
164
|
-
|
|
284
|
+
// Sticky wins over any duration, including one passed to show().
|
|
285
|
+
if (duration > 0 && this.mode !== "sticky") {
|
|
165
286
|
this.timeoutId = setTimeout(() => this.hide(), duration);
|
|
166
287
|
}
|
|
167
288
|
}
|
|
@@ -171,6 +292,42 @@ export class Toast extends BaseElement {
|
|
|
171
292
|
set message(value) {
|
|
172
293
|
this.setAttribute("message", value);
|
|
173
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Optional bold line above the message — "Upload failed" over "3 of 12 files
|
|
297
|
+
* could not be read". Named to match `box-alert`, which established the
|
|
298
|
+
* heading-plus-message shape in this family. Salesforce calls it `label`.
|
|
299
|
+
*/
|
|
300
|
+
get heading() {
|
|
301
|
+
return this.getAttribute("heading") ?? "";
|
|
302
|
+
}
|
|
303
|
+
set heading(value) {
|
|
304
|
+
this.setAttribute("heading", value);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Drop the outline for a softer, fill-only toast.
|
|
308
|
+
*
|
|
309
|
+
* Purely presentational and handled entirely in CSS, so it is deliberately
|
|
310
|
+
* not an observed attribute — there is nothing to re-render. The default
|
|
311
|
+
* stays bordered: that outline tracks box-ui-elements' `.notification` and
|
|
312
|
+
* is pinned by the colour conformance manifest.
|
|
313
|
+
*/
|
|
314
|
+
get borderless() {
|
|
315
|
+
return this.hasAttribute("borderless");
|
|
316
|
+
}
|
|
317
|
+
set borderless(value) {
|
|
318
|
+
this.toggleAttribute("borderless", value);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Whether the toast clears itself. `sticky` overrides any `duration`: a
|
|
322
|
+
* caller that has said "this one stays" should not have it taken away by a
|
|
323
|
+
* duration set elsewhere.
|
|
324
|
+
*/
|
|
325
|
+
get mode() {
|
|
326
|
+
return resolveToastMode(this.getAttribute("mode"));
|
|
327
|
+
}
|
|
328
|
+
set mode(value) {
|
|
329
|
+
this.setAttribute("mode", value);
|
|
330
|
+
}
|
|
174
331
|
get tone() {
|
|
175
332
|
return this.getAttribute("tone") ?? "info";
|
|
176
333
|
}
|
|
@@ -211,12 +368,20 @@ export class Toast extends BaseElement {
|
|
|
211
368
|
this.shadowRoot.innerHTML = `
|
|
212
369
|
<style>${toastStyles}</style>
|
|
213
370
|
<div part="toast" role="status" aria-live="polite">
|
|
214
|
-
<span part="
|
|
371
|
+
<span part="icon" aria-hidden="true"></span>
|
|
372
|
+
<div part="content">
|
|
373
|
+
<span part="tone-label" class="sr-only"></span>
|
|
374
|
+
<span part="heading"></span>
|
|
375
|
+
<span part="message"></span>
|
|
376
|
+
</div>
|
|
215
377
|
<slot name="action" part="action"></slot>
|
|
216
|
-
<button type="button" part="dismiss"
|
|
378
|
+
<button type="button" part="dismiss" aria-label="Dismiss">${DISMISS_ICON}</button>
|
|
217
379
|
</div>
|
|
218
380
|
`;
|
|
219
381
|
this.toastEl = this.shadowRoot.querySelector('[part="toast"]');
|
|
382
|
+
this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
|
|
383
|
+
this.headingEl = this.shadowRoot.querySelector('[part="heading"]');
|
|
384
|
+
this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
|
|
220
385
|
this.messageEl = this.shadowRoot.querySelector('[part="message"]');
|
|
221
386
|
this.actionSlot = this.shadowRoot.querySelector('slot[name="action"]');
|
|
222
387
|
this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
|
|
@@ -240,7 +405,18 @@ export class Toast extends BaseElement {
|
|
|
240
405
|
if (!visible) {
|
|
241
406
|
return;
|
|
242
407
|
}
|
|
243
|
-
|
|
408
|
+
const tone = this.tone;
|
|
409
|
+
this.toastEl.dataset.tone = tone;
|
|
410
|
+
// Only when it actually changes: this re-parses SVG markup, and update()
|
|
411
|
+
// runs on every attribute write.
|
|
412
|
+
if (tone !== this.renderedTone) {
|
|
413
|
+
this.iconEl.innerHTML = toneIcon(tone);
|
|
414
|
+
this.renderedTone = tone;
|
|
415
|
+
}
|
|
416
|
+
// The glyph is aria-hidden and the fill is colour, so without this the
|
|
417
|
+
// tone reaches a screen reader not at all.
|
|
418
|
+
this.toneLabelEl.textContent = toneAccessibleLabel(tone);
|
|
419
|
+
this.headingEl.textContent = this.heading;
|
|
244
420
|
this.messageEl.textContent = this.message;
|
|
245
421
|
}
|
|
246
422
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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 declare const toneAccessibleLabel: (tone: string) => string;
|