@aurodesignsystem-dev/auro-toast 0.0.0-pr147.14 → 0.0.0-pr147.15
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/package.json +2 -1
- package/src/auro-toast.js +485 -0
- package/src/auro-toaster.js +175 -0
- package/src/buttonVersion.js +1 -0
- package/src/iconVersion.js +1 -0
- package/src/index.js +4 -0
- package/src/registered.js +5 -0
- package/src/styles/color.scss +34 -0
- package/src/styles/style-toaster.scss +27 -0
- package/src/styles/style.scss +61 -0
- package/src/styles/tokens.scss +8 -0
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"================================================================================"
|
|
8
8
|
],
|
|
9
9
|
"name": "@aurodesignsystem-dev/auro-toast",
|
|
10
|
-
"version": "0.0.0-pr147.
|
|
10
|
+
"version": "0.0.0-pr147.15",
|
|
11
11
|
"description": "auro-toast HTML custom element",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"files": [
|
|
85
85
|
"dist/**/*",
|
|
86
86
|
"demo/**/*",
|
|
87
|
+
"src/**/*",
|
|
87
88
|
"custom-elements.json",
|
|
88
89
|
"CHANGELOG.md",
|
|
89
90
|
"README.md",
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// Copyright (c) 2023 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
/* eslint-disable lit/binding-positions, lit/no-invalid-html, max-lines */
|
|
7
|
+
import { LitElement } from "lit";
|
|
8
|
+
import { html } from "lit/static-html.js";
|
|
9
|
+
|
|
10
|
+
import { AuroDependencyVersioning } from "@aurodesignsystem/auro-library/scripts/runtime/dependencyTagVersioning.mjs";
|
|
11
|
+
import AuroLibraryRuntimeUtils from "@aurodesignsystem/auro-library/scripts/utils/runtimeUtils.mjs";
|
|
12
|
+
import { AuroButton } from "@aurodesignsystem/auro-button/class";
|
|
13
|
+
import { AuroIcon } from "@aurodesignsystem/auro-icon/class";
|
|
14
|
+
|
|
15
|
+
// Icons
|
|
16
|
+
import errorIcon from "@alaskaairux/icons/dist/icons/alert/error-stroke.mjs";
|
|
17
|
+
import infoIcon from "@alaskaairux/icons/dist/icons/alert/information-stroke.mjs";
|
|
18
|
+
import successIcon from "@alaskaairux/icons/dist/icons/interface/check-stroke.mjs";
|
|
19
|
+
import closeIcon from "@alaskaairux/icons/dist/icons/interface/x-lg.mjs";
|
|
20
|
+
|
|
21
|
+
import buttonVersion from "./buttonVersion.js";
|
|
22
|
+
import iconVersion from "./iconVersion.js";
|
|
23
|
+
|
|
24
|
+
import colorCss from "./styles/color.scss";
|
|
25
|
+
import styleCss from "./styles/style.scss";
|
|
26
|
+
import tokensCss from "./styles/tokens.scss";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIME_TIL_FADE_OUT = 5000;
|
|
29
|
+
const FADE_OUT_DURATION = 300;
|
|
30
|
+
|
|
31
|
+
// See https://git.io/JJ6SJ for "How to document your components using JSDoc"
|
|
32
|
+
/**
|
|
33
|
+
* The `auro-toast` element provides users a way to display short, temporary messages.
|
|
34
|
+
* @customElement auro-toast
|
|
35
|
+
*
|
|
36
|
+
* @csspart type-icon - Apply css to the toast type icon
|
|
37
|
+
* @csspart close-button - Apply css to the toast close button
|
|
38
|
+
* @fires onToastClose - **Deprecated**, use `toast-close` event instead.
|
|
39
|
+
* @event toast-close - Notifies that the toast has been closed
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// build the component class
|
|
43
|
+
export class AuroToast extends LitElement {
|
|
44
|
+
constructor() {
|
|
45
|
+
super();
|
|
46
|
+
|
|
47
|
+
this._initializeDefaults();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_initializeDefaults() {
|
|
51
|
+
/**
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
this.closeDom = new DOMParser().parseFromString(closeIcon.svg, "text/html");
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
this.closeSvg = this.closeDom.body.firstChild;
|
|
60
|
+
this.closeSvg.setAttribute("slot", "svg");
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
this.successDom = new DOMParser().parseFromString(
|
|
66
|
+
successIcon.svg,
|
|
67
|
+
"text/html",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
this.successSvg = this.successDom.body.firstChild;
|
|
74
|
+
|
|
75
|
+
this.successSvg.setAttribute("slot", "svg");
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
this.errorDom = new DOMParser().parseFromString(errorIcon.svg, "text/html");
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
this.errorSvg = this.errorDom.body.firstChild;
|
|
86
|
+
this.errorSvg.setAttribute("slot", "svg");
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
this.infoDom = new DOMParser().parseFromString(infoIcon.svg, "text/html");
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
this.infoSvg = this.infoDom.body.firstChild;
|
|
97
|
+
this.infoSvg.setAttribute("slot", "svg");
|
|
98
|
+
|
|
99
|
+
const versioning = new AuroDependencyVersioning();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
this.buttonTag = versioning.generateTag(
|
|
105
|
+
"auro-button",
|
|
106
|
+
buttonVersion,
|
|
107
|
+
AuroButton,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
this.iconTag = versioning.generateTag("auro-icon", iconVersion, AuroIcon);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
this.runtimeUtils = new AuroLibraryRuntimeUtils();
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
this.fadeOutTimer = undefined;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* True when the toast is not inside an auro-toaster and must manage its
|
|
127
|
+
* own live region announcement. Set once in connectedCallback.
|
|
128
|
+
* Default -- assumes toast is inside toaster.
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
this._isStandalone = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// This function is to define props used within the scope of this component
|
|
135
|
+
// Be sure to review https://lit.dev/docs/components/properties/
|
|
136
|
+
// to understand how to use reflected attributes with your property settings.
|
|
137
|
+
static get properties() {
|
|
138
|
+
return {
|
|
139
|
+
...LitElement.properties,
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Prevents the toast from auto-hiding on the default time.
|
|
143
|
+
*/
|
|
144
|
+
disableAutoHide: {
|
|
145
|
+
type: Boolean,
|
|
146
|
+
reflect: true,
|
|
147
|
+
attribute: "disableautohide",
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Removes icon from the toast UI.
|
|
152
|
+
*/
|
|
153
|
+
noIcon: {
|
|
154
|
+
type: Boolean,
|
|
155
|
+
reflect: true,
|
|
156
|
+
attribute: "noicon",
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sets the time in milliseconds until the toast hides.
|
|
161
|
+
*/
|
|
162
|
+
timeTilHide: {
|
|
163
|
+
type: Number,
|
|
164
|
+
reflect: true,
|
|
165
|
+
attribute: "timetilhide",
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The id of the element that triggered the toast.
|
|
170
|
+
* When the toast is manually closed, focus will return to this element.
|
|
171
|
+
* Takes precedence over the triggerElement property if both are set.
|
|
172
|
+
*/
|
|
173
|
+
trigger: {
|
|
174
|
+
type: String,
|
|
175
|
+
reflect: true,
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A direct reference to the element that triggered the toast.
|
|
180
|
+
* When the toast is manually closed, focus will return to this element.
|
|
181
|
+
* Use the trigger attribute instead if you prefer a declarative approach.
|
|
182
|
+
* @type {HTMLElement}
|
|
183
|
+
*/
|
|
184
|
+
triggerElement: {
|
|
185
|
+
type: Object,
|
|
186
|
+
attribute: false,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Component will render visually based on which variant value is set.
|
|
191
|
+
* @type {'error' | 'success' | 'custom'}
|
|
192
|
+
*/
|
|
193
|
+
variant: {
|
|
194
|
+
type: String,
|
|
195
|
+
reflect: true,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sets state of toast to visible
|
|
200
|
+
*/
|
|
201
|
+
visible: {
|
|
202
|
+
type: Boolean,
|
|
203
|
+
reflect: true,
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static get styles() {
|
|
209
|
+
return [styleCss, colorCss, tokensCss];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* This will register this element with the browser.
|
|
214
|
+
* @param {string} [name="auro-toast"] - The name of the element that you want to register.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* AuroToast.register("custom-toast") // this will register this element to <custom-toast/>
|
|
218
|
+
*
|
|
219
|
+
*/
|
|
220
|
+
static register(name = "auro-toast") {
|
|
221
|
+
AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroToast);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
firstUpdated() {
|
|
225
|
+
// Add the tag name as an attribute if it is different than the component name
|
|
226
|
+
this.runtimeUtils.handleComponentTagRename(this, "auro-toast");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns focus to the trigger element when the toast is manually closed.
|
|
231
|
+
* Not called on auto-dismiss — moving focus during auto-dismiss would
|
|
232
|
+
* interrupt the AT user's current position in the page.
|
|
233
|
+
* @private
|
|
234
|
+
* @returns {void}
|
|
235
|
+
*/
|
|
236
|
+
_returnFocus() {
|
|
237
|
+
const target = (this.trigger ? document.getElementById(this.trigger) : null) ??
|
|
238
|
+
this.triggerElement;
|
|
239
|
+
|
|
240
|
+
if (target) {
|
|
241
|
+
target.focus();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @private
|
|
247
|
+
* @returns {void}
|
|
248
|
+
*/
|
|
249
|
+
clickToClose() {
|
|
250
|
+
this._returnFocus();
|
|
251
|
+
this.closeToast();
|
|
252
|
+
clearTimeout(this.fadeOutTimer);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @private
|
|
257
|
+
* @returns {void}
|
|
258
|
+
*/
|
|
259
|
+
fadeOutToast() {
|
|
260
|
+
if (!this.disableAutoHide) {
|
|
261
|
+
const toastContainer = this.shadowRoot.querySelector(".toastContainer");
|
|
262
|
+
if (toastContainer) {
|
|
263
|
+
toastContainer.classList.add("hidden");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
this.closeToast();
|
|
268
|
+
}, FADE_OUT_DURATION);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @private
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*/
|
|
276
|
+
closeToast() {
|
|
277
|
+
clearTimeout(this.fadeOutTimer);
|
|
278
|
+
this.visible = false;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Emits closed toast event.
|
|
282
|
+
*
|
|
283
|
+
* @deprecated Use `toast-close` event instead.
|
|
284
|
+
* @event onToastClose
|
|
285
|
+
* @type {Object}
|
|
286
|
+
*/
|
|
287
|
+
this.dispatchEvent(
|
|
288
|
+
new CustomEvent("onToastClose", {
|
|
289
|
+
bubbles: true,
|
|
290
|
+
composed: true,
|
|
291
|
+
detail: this, // unchanged for backwards compatibility; use `toast-close` for the new `{ visible }` shape
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Emits closed toast event.
|
|
297
|
+
*
|
|
298
|
+
* @event toast-close
|
|
299
|
+
* @type {{ visible: boolean }}
|
|
300
|
+
*/
|
|
301
|
+
this.dispatchEvent(
|
|
302
|
+
new CustomEvent("toast-close", {
|
|
303
|
+
bubbles: true,
|
|
304
|
+
composed: true,
|
|
305
|
+
detail: { visible: false },
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* For mobile, set the onclick function so the toast can be dismissed if it is tapped on anywhere inside the toast.
|
|
312
|
+
* @private
|
|
313
|
+
* @returns {void}
|
|
314
|
+
*/
|
|
315
|
+
setOnClick() {
|
|
316
|
+
const mobileBreakPoint = 767;
|
|
317
|
+
if (window.innerWidth < mobileBreakPoint) {
|
|
318
|
+
this.onclick = () => {
|
|
319
|
+
this.fadeOutToast();
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Determines which type icon to render based on the variant prop.
|
|
326
|
+
* @private
|
|
327
|
+
* @returns {HTMLElement} - The icon element to render.
|
|
328
|
+
*/
|
|
329
|
+
getVariantIcon() {
|
|
330
|
+
const VARIANT_ICONS = {
|
|
331
|
+
success: this.successSvg,
|
|
332
|
+
error: this.errorSvg,
|
|
333
|
+
default: this.infoSvg,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return VARIANT_ICONS[this.variant] || VARIANT_ICONS.default;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
connectedCallback() {
|
|
340
|
+
super.connectedCallback();
|
|
341
|
+
this.setOnClick();
|
|
342
|
+
|
|
343
|
+
// Dispatch a cancelable event so auro-toaster can signal it will handle
|
|
344
|
+
// announcements. If nothing cancels the event the toast is standalone and
|
|
345
|
+
// must register its own live region on the host element — before
|
|
346
|
+
// any visibility change — so the AT has already observed it as a live
|
|
347
|
+
// region by the time content is rendered into the shadow DOM.
|
|
348
|
+
const event = new CustomEvent('toast-request-announce', {
|
|
349
|
+
bubbles: true,
|
|
350
|
+
cancelable: true,
|
|
351
|
+
composed: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// NOTE — reverse logic:
|
|
355
|
+
// dispatchEvent() returns TRUE when nobody called preventDefault(), which
|
|
356
|
+
// means no auro-toaster was listening — the toast is standalone and must
|
|
357
|
+
// own its live region. It returns FALSE when a toaster cancelled the event,
|
|
358
|
+
// signalling that the toaster's aria-live region will handle announcements.
|
|
359
|
+
const eventNotPrevented = this.dispatchEvent(event);
|
|
360
|
+
|
|
361
|
+
// Secondary check: even if no toaster answered the event, the toast may
|
|
362
|
+
// be inside another element that already owns a live region (e.g. a plain
|
|
363
|
+
// div[aria-live="polite"]). Setting a role on the host in that case would
|
|
364
|
+
// create a nested live region. aria-live="off" is intentionally excluded
|
|
365
|
+
// — it disables live region behavior and is not an active announcer.
|
|
366
|
+
this._isStandalone = eventNotPrevented && !this._hasAncestorLiveRegion();
|
|
367
|
+
|
|
368
|
+
this._syncStandaloneRole();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Sets or removes the live-region role on the host depending on whether
|
|
373
|
+
* the toast is standalone. Extracted so connectedCallback and updated()
|
|
374
|
+
* stay in sync without duplicating the logic.
|
|
375
|
+
* @private
|
|
376
|
+
* @returns {void}
|
|
377
|
+
*/
|
|
378
|
+
_syncStandaloneRole() {
|
|
379
|
+
if (this._isStandalone) {
|
|
380
|
+
this.setAttribute('role', this.variant === 'error' ? 'alert' : 'status');
|
|
381
|
+
} else {
|
|
382
|
+
this.removeAttribute('role');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Walks the DOM ancestors (crossing shadow boundaries) looking for any
|
|
388
|
+
* active live region — either an aria-live="polite/assertive" attribute or
|
|
389
|
+
* an implicit live region role (alert, status, log).
|
|
390
|
+
* aria-live="off" is not considered active and is intentionally ignored.
|
|
391
|
+
* @private
|
|
392
|
+
* @returns {boolean}
|
|
393
|
+
*/
|
|
394
|
+
_hasAncestorLiveRegion() {
|
|
395
|
+
let node = this.parentElement;
|
|
396
|
+
while (node) {
|
|
397
|
+
if (
|
|
398
|
+
['polite', 'assertive'].includes(node.getAttribute?.('aria-live')) ||
|
|
399
|
+
['alert', 'status', 'log'].includes(node.getAttribute?.('role'))
|
|
400
|
+
) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
// Traverse up, crossing shadow DOM boundaries if needed
|
|
404
|
+
node = node.parentElement ?? node.getRootNode()?.host ?? null;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
updated(changedProperties) {
|
|
410
|
+
if (changedProperties.has("visible")) {
|
|
411
|
+
this.handleSlotContent();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Keep the standalone role in sync if variant changes after connection.
|
|
415
|
+
if (changedProperties.has("variant")) {
|
|
416
|
+
clearTimeout(this.fadeOutTimer);
|
|
417
|
+
this._syncStandaloneRole();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// do not auto dismiss for error toasts or if disableAutoHide is set
|
|
421
|
+
if (this.visible && !this.disableAutoHide && this.variant !== "error") {
|
|
422
|
+
clearTimeout(this.fadeOutTimer);
|
|
423
|
+
this.fadeOutTimer = setTimeout(() => {
|
|
424
|
+
this.fadeOutToast();
|
|
425
|
+
}, this.timeTilHide || DEFAULT_TIME_TIL_FADE_OUT);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Mirrors any defined custom type svg into the correct place in the template.
|
|
431
|
+
* @private
|
|
432
|
+
* @returns {void}
|
|
433
|
+
*/
|
|
434
|
+
handleSlotContent() {
|
|
435
|
+
try {
|
|
436
|
+
const customSvg = this.querySelector(`[slot="customSvg"]`);
|
|
437
|
+
|
|
438
|
+
if (customSvg) {
|
|
439
|
+
const iconSvg = customSvg.cloneNode(true);
|
|
440
|
+
const typeIcon = this.shadowRoot.querySelector(".typeIcon");
|
|
441
|
+
|
|
442
|
+
if (typeIcon) {
|
|
443
|
+
iconSvg.setAttribute("slot", "svg");
|
|
444
|
+
typeIcon.appendChild(iconSvg);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error("handleSlotContent", error); // eslint-disable-line no-console
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
render() {
|
|
453
|
+
return html`
|
|
454
|
+
${this.visible
|
|
455
|
+
? html`
|
|
456
|
+
<div class="toastContainer">
|
|
457
|
+
${this.noIcon
|
|
458
|
+
? undefined
|
|
459
|
+
: html`
|
|
460
|
+
<${this.iconTag} customColor customSvg class="typeIcon body-default" part="type-icon">
|
|
461
|
+
${this.variant === "custom" ? undefined : html`${this.getVariantIcon()}`}
|
|
462
|
+
</${this.iconTag}>
|
|
463
|
+
`
|
|
464
|
+
}
|
|
465
|
+
<div class="message body-default"><slot></slot></div>
|
|
466
|
+
<${this.buttonTag}
|
|
467
|
+
variant="flat"
|
|
468
|
+
shape="circle"
|
|
469
|
+
size="xs"
|
|
470
|
+
appearance=${this.variant !== "error" && this.variant !== "success" ? "inverse" : this.appearance}
|
|
471
|
+
@click="${this.clickToClose}"
|
|
472
|
+
part="close-button"
|
|
473
|
+
class="closeButton">
|
|
474
|
+
<${this.iconTag} customColor customSvg>
|
|
475
|
+
${this.closeSvg}
|
|
476
|
+
</${this.iconTag}>
|
|
477
|
+
<span slot="ariaLabel">Close this notification.</span>
|
|
478
|
+
</${this.buttonTag}>
|
|
479
|
+
</div>
|
|
480
|
+
`
|
|
481
|
+
: undefined
|
|
482
|
+
}
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Copyright (c) 2023 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
import { html, LitElement } from "lit";
|
|
7
|
+
import AuroLibraryRuntimeUtils from "@aurodesignsystem/auro-library/scripts/utils/runtimeUtils.mjs";
|
|
8
|
+
import styleCss from "./styles/style-toaster.scss";
|
|
9
|
+
|
|
10
|
+
export class AuroToaster extends LitElement {
|
|
11
|
+
static get styles() {
|
|
12
|
+
return [styleCss];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
// Initialised in the constructor so they survive disconnect/reconnect.
|
|
18
|
+
// If these were in connectedCallback, a reconnected toaster would forget
|
|
19
|
+
// which error toasts it had already announced and could re-announce them.
|
|
20
|
+
this._assertiveResetTimer = undefined;
|
|
21
|
+
this._announcedErrorToasts = new WeakSet();
|
|
22
|
+
|
|
23
|
+
// Cancel the standalone live region request from any child toast — the
|
|
24
|
+
// toaster's own aria-live region handles all announcements.
|
|
25
|
+
// Calling preventDefault() causes dispatchEvent() on the toast to return
|
|
26
|
+
// false, which the toast reads as "a toaster is present — do not set a
|
|
27
|
+
// standalone live region role on the host element."
|
|
28
|
+
this._onToastRequestAnnounce = (e) => e.preventDefault();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This will register this element with the browser.
|
|
33
|
+
* @param {string} [name="auro-toaster"] - The name of the element that you want to register to.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* AuroToaster.register("custom-toaster") // this will register this element to <custom-toaster/>
|
|
37
|
+
*
|
|
38
|
+
*/
|
|
39
|
+
static register(name = "auro-toaster") {
|
|
40
|
+
AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroToaster);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
connectedCallback() {
|
|
44
|
+
super.connectedCallback();
|
|
45
|
+
this.addEventListener('toast-request-announce', this._onToastRequestAnnounce);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @private
|
|
49
|
+
*
|
|
50
|
+
* Observes toast attribute changes to determine when we should temporarily
|
|
51
|
+
* switch the live region from "polite" → "assertive".
|
|
52
|
+
*
|
|
53
|
+
* Supported trigger paths:
|
|
54
|
+
* 1. Normal path:
|
|
55
|
+
* - A toast becomes visible
|
|
56
|
+
* - AND it is already an error toast
|
|
57
|
+
* 2. Defensive fallback (unsupported but possible misuse):
|
|
58
|
+
* - A visible toast has its variant changed to "error"
|
|
59
|
+
*
|
|
60
|
+
* Note:
|
|
61
|
+
* Toasts are not intended to mutate while visible. This fallback exists
|
|
62
|
+
* only to preserve accessibility behavior if that contract is violated.
|
|
63
|
+
*/
|
|
64
|
+
this._observer = new MutationObserver((mutations) => {
|
|
65
|
+
for (const mutation of mutations) {
|
|
66
|
+
const target = mutation.target;
|
|
67
|
+
|
|
68
|
+
// Only react to auro-toast elements (including versioned tags like
|
|
69
|
+
// auro-toast_1_2_3). Explicitly ignore auro-toaster itself.
|
|
70
|
+
if (!/^auro-toast(_|$)/u.test(target.localName ?? '')) continue;
|
|
71
|
+
|
|
72
|
+
const isVisible = target.hasAttribute?.('visible');
|
|
73
|
+
const isError = target.getAttribute?.('variant') === 'error';
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------
|
|
76
|
+
// Case 1: supported behavior
|
|
77
|
+
// A toast becomes visible AND is already an error toast
|
|
78
|
+
// ---------------------------------------------------------------------
|
|
79
|
+
const becameVisible =
|
|
80
|
+
mutation.attributeName === 'visible' &&
|
|
81
|
+
isVisible &&
|
|
82
|
+
isError;
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------
|
|
85
|
+
// Case 2: defensive fallback
|
|
86
|
+
// A toast is already visible and its variant is changed to "error"
|
|
87
|
+
// (unsupported usage pattern, but we handle it safely for a11y)
|
|
88
|
+
// ---------------------------------------------------------------------
|
|
89
|
+
const becameErrorWhileVisible =
|
|
90
|
+
mutation.attributeName === 'variant' &&
|
|
91
|
+
mutation.oldValue !== 'error' &&
|
|
92
|
+
isVisible &&
|
|
93
|
+
isError;
|
|
94
|
+
|
|
95
|
+
if (becameVisible || becameErrorWhileVisible) {
|
|
96
|
+
this._setAssertiveTemporarily();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this._observer.observe(this, {
|
|
102
|
+
subtree: true,
|
|
103
|
+
|
|
104
|
+
// REQUIRED: without this, attribute changes will not trigger the observer
|
|
105
|
+
attributes: true,
|
|
106
|
+
|
|
107
|
+
// We only care about these two attributes for performance and clarity
|
|
108
|
+
attributeFilter: ['visible', 'variant'],
|
|
109
|
+
|
|
110
|
+
// REQUIRED for detecting transitions (e.g. non-error → error)
|
|
111
|
+
attributeOldValue: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
disconnectedCallback() {
|
|
116
|
+
super.disconnectedCallback();
|
|
117
|
+
this._observer?.disconnect();
|
|
118
|
+
clearTimeout(this._assertiveResetTimer);
|
|
119
|
+
this.removeEventListener('toast-request-announce', this._onToastRequestAnnounce);
|
|
120
|
+
|
|
121
|
+
// Reset the live region to polite so a reconnected toaster does not
|
|
122
|
+
// inherit a stale assertive state left behind by a cleared timer.
|
|
123
|
+
const liveRegion = this.shadowRoot?.querySelector('[aria-live]');
|
|
124
|
+
if (liveRegion) {
|
|
125
|
+
liveRegion.setAttribute('aria-live', 'polite');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Temporarily sets the live region to assertive so the error toast
|
|
131
|
+
* interrupts the screen reader, then resets to polite after 3 seconds.
|
|
132
|
+
* This ensures subsequent polite toasts don't keep interrupting the user,
|
|
133
|
+
* even if the error toast remains visible (errors are never auto-dismissed).
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
_setAssertiveTemporarily() {
|
|
137
|
+
const liveRegion = this.shadowRoot?.querySelector('[aria-live]');
|
|
138
|
+
if (!liveRegion) return;
|
|
139
|
+
|
|
140
|
+
clearTimeout(this._assertiveResetTimer);
|
|
141
|
+
liveRegion.setAttribute('aria-live', 'assertive');
|
|
142
|
+
|
|
143
|
+
this._assertiveResetTimer = setTimeout(() => {
|
|
144
|
+
liveRegion.setAttribute('aria-live', 'polite');
|
|
145
|
+
}, 3000);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render() {
|
|
149
|
+
return html`
|
|
150
|
+
<div class="toaster-wrapper" aria-live="polite" aria-atomic="false">
|
|
151
|
+
<slot @slotchange="${this._onSlotChange}"></slot>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handles slot changes — checks if a newly slotted error toast
|
|
158
|
+
* is already visible at the time it is added to the toaster.
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
_onSlotChange(e) {
|
|
162
|
+
const newErrorToast = e.target.assignedElements({ flatten: true })
|
|
163
|
+
.find(el =>
|
|
164
|
+
/^auro-toast(_|$)/u.test(el.localName ?? '') &&
|
|
165
|
+
el.getAttribute('variant') === 'error' &&
|
|
166
|
+
el.hasAttribute('visible') &&
|
|
167
|
+
!this._announcedErrorToasts.has(el)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (newErrorToast) {
|
|
171
|
+
this._announcedErrorToasts.add(newErrorToast);
|
|
172
|
+
this._setAssertiveTemporarily();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "12.3.2";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "9.2.0";
|
package/src/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) 2023 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
// Import Auro tokens
|
|
7
|
+
@use "@aurodesignsystem/design-tokens/dist/themes/alaska/SCSSVariables--alaska" as v;
|
|
8
|
+
|
|
9
|
+
:host([visible]) {
|
|
10
|
+
background-color: var(--ds-auro-toast-container-color);
|
|
11
|
+
color: var(--ds-auro-toast-text-color);
|
|
12
|
+
|
|
13
|
+
.typeIcon {
|
|
14
|
+
color: var(--ds-auro-toast-icon-color);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.closeButton {
|
|
18
|
+
&:hover {
|
|
19
|
+
background-color: var(--ds-auro-toast-close-button-hover-container-color);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:host([variant='error']) {
|
|
25
|
+
--ds-auro-toast-container-color: var(--ds-basic-color-status-error-subtle, #{v.$ds-basic-color-status-error-subtle});
|
|
26
|
+
--ds-auro-toast-icon-color: var(--ds-basic-color-status-error, #{v.$ds-basic-color-status-error});
|
|
27
|
+
--ds-auro-toast-text-color: var(--ds-basic-color-texticon-default, #{v.$ds-basic-color-texticon-default});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:host([variant='success']) {
|
|
31
|
+
--ds-auro-toast-container-color: var(--ds-basic-color-status-success-subtle, #{v.$ds-basic-color-status-success-subtle});
|
|
32
|
+
--ds-auro-toast-icon-color: var(--ds-basic-color-status-success, #{v.$ds-basic-color-status-success});
|
|
33
|
+
--ds-auro-toast-text-color: var(--ds-basic-color-texticon-default, #{v.$ds-basic-color-texticon-default});
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) 2023 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
// Import Auro tokens
|
|
7
|
+
@use "@aurodesignsystem/design-tokens/dist/legacy/auro-classic/SCSSVariables" as vac;
|
|
8
|
+
|
|
9
|
+
// container styles - defines the look of the outer custom component
|
|
10
|
+
.toaster-wrapper {
|
|
11
|
+
position: fixed;
|
|
12
|
+
z-index: var(--ds-depth-modal, vac.$ds-depth-modal);
|
|
13
|
+
right: 2%;
|
|
14
|
+
bottom: 2%;
|
|
15
|
+
left: 2%;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column-reverse;
|
|
18
|
+
align-items: flex-end;
|
|
19
|
+
gap: var(--ds-size-200, vac.$ds-size-200);
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
|
|
22
|
+
@media (width >= #{vac.$ds-grid-breakpoint-lg}) {
|
|
23
|
+
right: 1%;
|
|
24
|
+
left: unset;
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Copyright (c) 2023 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
// Import Auro tokens
|
|
7
|
+
@use "@aurodesignsystem/design-tokens/dist/legacy/auro-classic/SCSSVariables" as vac;
|
|
8
|
+
|
|
9
|
+
@use '@aurodesignsystem/webcorestylesheets/dist/bundled/type/classes.alaska.min.css';
|
|
10
|
+
|
|
11
|
+
// display: inherit keeps the host in the accessibility tree when not visible.
|
|
12
|
+
// display: none removes it entirely from the accessibility tree —
|
|
13
|
+
//
|
|
14
|
+
// This is used to prevent visual artifacts from toasts that remain in the DOM
|
|
15
|
+
// after hiding.
|
|
16
|
+
:host {
|
|
17
|
+
display: inherit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:host([visible]) {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
min-width: 100%;
|
|
23
|
+
border-radius: var(--ds-size-100, vac.$ds-size-100);
|
|
24
|
+
box-shadow: var(--ds-elevation-200, vac.$ds-elevation-200);
|
|
25
|
+
pointer-events: auto;
|
|
26
|
+
|
|
27
|
+
// Phones (< sm) keep the full-width sizing from the base rule above.
|
|
28
|
+
// From the sm breakpoint up, use fixed pixel bounds so the toast is sized
|
|
29
|
+
// consistently across tablet and desktop widths instead of shrinking to a
|
|
30
|
+
// small fraction of the available space.
|
|
31
|
+
@media (width >= #{vac.$ds-grid-breakpoint-sm}) {
|
|
32
|
+
min-width: 200px;
|
|
33
|
+
max-width: 500px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.toastContainer {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: row;
|
|
39
|
+
align-items: flex-start;
|
|
40
|
+
padding: var(--ds-size-200, vac.$ds-size-200) var(--ds-size-150, vac.$ds-size-150);
|
|
41
|
+
gap: var(--ds-size-200, vac.$ds-size-200);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hidden {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transition: visibility 0s 300ms, opacity 300ms ease-out;
|
|
47
|
+
visibility: hidden;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[auro-icon] {
|
|
51
|
+
--ds-auro-icon-size: var(--ds-size-300, #{vac.$ds-size-300});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.message {
|
|
55
|
+
flex: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.closeButton {
|
|
59
|
+
border-radius: 50%;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@use "@aurodesignsystem/design-tokens/dist/themes/alaska/SCSSVariables--alaska" as v;
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
--ds-auro-toast-close-button-hover-container-color: var(--ds-advanced-color-button-tertiary-background-hover, #{v.$ds-advanced-color-button-tertiary-background-hover});
|
|
5
|
+
--ds-auro-toast-container-color: var(--ds-advanced-color-shared-background-strong, #{v.$ds-advanced-color-shared-background-strong});
|
|
6
|
+
--ds-auro-toast-icon-color: var(--ds-basic-color-texticon-inverse, #{v.$ds-basic-color-texticon-inverse});
|
|
7
|
+
--ds-auro-toast-text-color: var(--ds-basic-color-texticon-inverse, #{v.$ds-basic-color-texticon-inverse});
|
|
8
|
+
}
|