@jungherz-de/glasskit-elements 1.18.0 → 1.19.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/README.md +4 -4
- package/dist/components/base.js +7 -2
- package/dist/components/glk-toast.js +199 -35
- package/dist/glasskit-elements.esm.js +208 -39
- package/dist/glasskit-elements.js +208 -39
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +7 -2
- package/src/components/feedback/glk-toast.js +199 -35
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-44cc11?style=flat-square" alt="Vanilla JS"></a>
|
|
6
6
|
<a href="#"><img src="https://img.shields.io/badge/components-33-7ec8e3?style=flat-square" alt="33 Components"></a>
|
|
7
7
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License"></a>
|
|
8
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.
|
|
8
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.19.0-lightgrey?style=flat-square" alt="Changelog"></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@jungherz-de/glasskit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fglasskit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
@@ -66,7 +66,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
|
|
|
66
66
|
|
|
67
67
|
```html
|
|
68
68
|
<!-- 1. GlassKit CSS -->
|
|
69
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.
|
|
69
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.19/glasskit.min.css">
|
|
70
70
|
|
|
71
71
|
<!-- 2. GlassKit Elements -->
|
|
72
72
|
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
|
|
@@ -104,7 +104,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
|
|
|
104
104
|
<!DOCTYPE html>
|
|
105
105
|
<html data-theme="dark">
|
|
106
106
|
<head>
|
|
107
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.
|
|
107
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.19/glasskit.min.css">
|
|
108
108
|
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
|
|
109
109
|
</head>
|
|
110
110
|
<body>
|
|
@@ -174,7 +174,7 @@ All form components support `name`, `value`, `disabled` and participate in nativ
|
|
|
174
174
|
|---|---|---|
|
|
175
175
|
| `<glk-progress>` | Progress bar | `value`, `label`, `variant` (success, error), `size` (sm, lg) |
|
|
176
176
|
| `<glk-modal>` | Modal dialog | `open`, `title` |
|
|
177
|
-
| `<glk-toast>` |
|
|
177
|
+
| `<glk-toast>` | Notification that hides itself, or an offer with one action and an × | `message`, `variant` (success, error, warning), `duration`, `action-label`, `action-value`, `dismissible` |
|
|
178
178
|
| `<glk-popover>` | Anchored dropdown / menu | `open`, `placement` (top, bottom, start, end) |
|
|
179
179
|
| `<glk-sheet>` | Bottom sheet, the mobile sibling of the modal | `open`, `inline`, `title` |
|
|
180
180
|
|
package/dist/components/base.js
CHANGED
|
@@ -220,10 +220,15 @@ class GlkElement extends HTMLElement {
|
|
|
220
220
|
return el;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Dispatch a bubbling, composed CustomEvent. With { cancelable: true } a
|
|
225
|
+
* listener may call preventDefault(); the return value is false then.
|
|
226
|
+
*/
|
|
227
|
+
emit(eventName, detail = null, { cancelable = false } = {}) {
|
|
228
|
+
return this.dispatchEvent(new CustomEvent(eventName, {
|
|
225
229
|
bubbles: true,
|
|
226
230
|
composed: true,
|
|
231
|
+
cancelable,
|
|
227
232
|
detail
|
|
228
233
|
}));
|
|
229
234
|
}
|
|
@@ -1,35 +1,106 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
|
+
// A short message at the top of the screen that hides itself — and, since
|
|
5
|
+
// 1.19.0, one that offers something: "A new version · Reload".
|
|
6
|
+
//
|
|
7
|
+
// Attributes: message, variant (success | error | warning), duration (ms;
|
|
8
|
+
// 0 keeps it up), visible, action-label, action-value (reported
|
|
9
|
+
// in glk-action; the label when missing), dismissible (an ×
|
|
10
|
+
// without an action), close-label (name of the ×, "Close" by
|
|
11
|
+
// default)
|
|
12
|
+
// Slot: icon — replaces the built-in icon; leave its stroke unset and
|
|
13
|
+
// it takes the variant colour
|
|
14
|
+
// Methods: show(message, variant, duration)
|
|
15
|
+
// show(message, { variant, duration, action: { label, value }, dismissible })
|
|
16
|
+
// dismiss()
|
|
17
|
+
// Events: glk-dismiss — hidden after its duration
|
|
18
|
+
// glk-action { action, label } — the action was used; the toast
|
|
19
|
+
// closes unless a listener calls preventDefault() or shows the
|
|
20
|
+
// next message from the handler
|
|
21
|
+
// glk-close — closed with the × or Escape
|
|
22
|
+
//
|
|
23
|
+
// A toast with an action brings an × and stays until one of them is used,
|
|
24
|
+
// unless a duration is set; while the pointer or the focus is on a toast
|
|
25
|
+
// with buttons, it does not time out. It is a polite live region, so the
|
|
26
|
+
// message is announced; hidden, its buttons are inert. Closing it from the
|
|
27
|
+
// keyboard hands the focus back to where it came from.
|
|
28
|
+
|
|
4
29
|
const VARIANTS = ['success', 'error', 'warning'];
|
|
30
|
+
const DEFAULT_DURATION = 3000;
|
|
5
31
|
|
|
32
|
+
// Stroke, fill and caps come from .glass-toast__icon, so the variant colours
|
|
33
|
+
// the icon — an own stroke attribute here would win over the inherited one.
|
|
6
34
|
const ICONS = {
|
|
7
|
-
success:
|
|
8
|
-
error:
|
|
9
|
-
warning:
|
|
35
|
+
success: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
36
|
+
error: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
|
|
37
|
+
warning: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
|
|
10
38
|
};
|
|
39
|
+
const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
|
|
11
40
|
|
|
12
41
|
class GlkToast extends GlkElement {
|
|
13
42
|
static get observedAttributes() {
|
|
14
|
-
return ['message', 'variant', 'duration', 'visible'];
|
|
43
|
+
return ['message', 'variant', 'duration', 'visible', 'action-label', 'action-value', 'dismissible', 'close-label'];
|
|
15
44
|
}
|
|
16
45
|
|
|
17
46
|
render() {
|
|
18
|
-
this._toast = this.createElement('div', this._computeClasses());
|
|
47
|
+
this._toast = this.createElement('div', this._computeClasses(), { role: 'status', 'aria-live': 'polite' });
|
|
19
48
|
|
|
20
49
|
this._iconEl = this.createElement('span', ['glass-toast__icon']);
|
|
50
|
+
this._iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
51
|
+
this._iconEl.appendChild(this._iconSlot);
|
|
21
52
|
this._textEl = this.createElement('span', ['glass-toast__text']);
|
|
22
53
|
this._textEl.textContent = this.getAttribute('message') || '';
|
|
54
|
+
this._actionBtn = this.createElement('button', ['glass-toast__action'], { type: 'button' });
|
|
55
|
+
this._closeBtn = this.createElement('button', ['glass-toast__close'], { type: 'button' });
|
|
56
|
+
this._closeBtn.innerHTML = CLOSE_ICON;
|
|
23
57
|
|
|
24
58
|
this._updateIcon();
|
|
25
|
-
|
|
26
|
-
this._toast.appendChild(this._iconEl);
|
|
27
|
-
this._toast.appendChild(this._textEl);
|
|
59
|
+
this._toast.append(this._iconEl, this._textEl);
|
|
28
60
|
this._wrapper.appendChild(this._toast);
|
|
61
|
+
this._syncControls();
|
|
29
62
|
|
|
30
|
-
if (this.getBoolAttr('visible'))
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
if (this.getBoolAttr('visible')) this._show();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setupEvents() {
|
|
67
|
+
this._onClick = (event) => {
|
|
68
|
+
const path = event.composedPath();
|
|
69
|
+
if (path.includes(this._closeBtn)) this._close();
|
|
70
|
+
else if (path.includes(this._actionBtn)) this._takeAction();
|
|
71
|
+
};
|
|
72
|
+
this._onKeydown = (event) => {
|
|
73
|
+
if (event.key === 'Escape' && this._hasButtons()) { event.stopPropagation(); this._close(); }
|
|
74
|
+
};
|
|
75
|
+
// While the pointer or the focus is on a toast with buttons, it waits.
|
|
76
|
+
this._onHold = (event) => {
|
|
77
|
+
if (event.type === 'focusin' && !this._toast.contains(event.relatedTarget)) this._returnFocus = event.relatedTarget;
|
|
78
|
+
if (!this._hasButtons()) return;
|
|
79
|
+
this._held = true;
|
|
80
|
+
clearTimeout(this._timer);
|
|
81
|
+
};
|
|
82
|
+
this._onRelease = (event) => {
|
|
83
|
+
if (event.type === 'focusout' && this._toast.contains(event.relatedTarget)) return;
|
|
84
|
+
this._held = this._toast.matches(':hover') || this._toast.contains(this._shadow.activeElement);
|
|
85
|
+
if (!this._held) this._arm();
|
|
86
|
+
};
|
|
87
|
+
this._toast.addEventListener('click', this._onClick);
|
|
88
|
+
this._toast.addEventListener('keydown', this._onKeydown);
|
|
89
|
+
this._toast.addEventListener('pointerenter', this._onHold);
|
|
90
|
+
this._toast.addEventListener('focusin', this._onHold);
|
|
91
|
+
this._toast.addEventListener('pointerleave', this._onRelease);
|
|
92
|
+
this._toast.addEventListener('focusout', this._onRelease);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
teardownEvents() {
|
|
96
|
+
clearTimeout(this._timer);
|
|
97
|
+
if (!this._toast) return;
|
|
98
|
+
this._toast.removeEventListener('click', this._onClick);
|
|
99
|
+
this._toast.removeEventListener('keydown', this._onKeydown);
|
|
100
|
+
this._toast.removeEventListener('pointerenter', this._onHold);
|
|
101
|
+
this._toast.removeEventListener('focusin', this._onHold);
|
|
102
|
+
this._toast.removeEventListener('pointerleave', this._onRelease);
|
|
103
|
+
this._toast.removeEventListener('focusout', this._onRelease);
|
|
33
104
|
}
|
|
34
105
|
|
|
35
106
|
onAttributeChanged(name) {
|
|
@@ -43,11 +114,14 @@ class GlkToast extends GlkElement {
|
|
|
43
114
|
this._updateIcon();
|
|
44
115
|
break;
|
|
45
116
|
case 'visible':
|
|
46
|
-
if (this.getBoolAttr('visible'))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
if (this.getBoolAttr('visible')) this._show();
|
|
118
|
+
else this._hide();
|
|
119
|
+
break;
|
|
120
|
+
case 'action-label':
|
|
121
|
+
case 'action-value':
|
|
122
|
+
case 'dismissible':
|
|
123
|
+
case 'close-label':
|
|
124
|
+
this._syncControls();
|
|
51
125
|
break;
|
|
52
126
|
}
|
|
53
127
|
}
|
|
@@ -55,22 +129,55 @@ class GlkToast extends GlkElement {
|
|
|
55
129
|
_computeClasses() {
|
|
56
130
|
const classes = ['glass-toast'];
|
|
57
131
|
const variant = this.getAttribute('variant');
|
|
58
|
-
if (variant && VARIANTS.includes(variant)) {
|
|
59
|
-
|
|
60
|
-
}
|
|
132
|
+
if (variant && VARIANTS.includes(variant)) classes.push(`glass-toast--${variant}`);
|
|
133
|
+
if (this.getBoolAttr('visible')) classes.push('is-visible');
|
|
61
134
|
return classes;
|
|
62
135
|
}
|
|
63
136
|
|
|
64
137
|
_updateIcon() {
|
|
65
|
-
|
|
66
|
-
this.
|
|
138
|
+
// Fallback content of the icon slot: shown while nothing is slotted.
|
|
139
|
+
this._iconSlot.innerHTML = ICONS[this.getAttribute('variant')] || ICONS.success;
|
|
67
140
|
}
|
|
68
141
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
142
|
+
_hasButtons() { return this._actionBtn.isConnected || this._closeBtn.isConnected; }
|
|
143
|
+
|
|
144
|
+
// The action and the × sit in the tree only when they are wanted: an
|
|
145
|
+
// action brings its ×, `dismissible` brings the × alone.
|
|
146
|
+
_syncControls() {
|
|
147
|
+
const label = this.getAttribute('action-label');
|
|
148
|
+
if (label) {
|
|
149
|
+
this._actionBtn.textContent = label;
|
|
150
|
+
if (!this._actionBtn.isConnected) this._textEl.after(this._actionBtn);
|
|
151
|
+
} else {
|
|
152
|
+
this._actionBtn.remove();
|
|
153
|
+
}
|
|
154
|
+
if (label || this.getBoolAttr('dismissible')) {
|
|
155
|
+
if (!this._closeBtn.isConnected) this._toast.appendChild(this._closeBtn);
|
|
156
|
+
} else {
|
|
157
|
+
this._closeBtn.remove();
|
|
158
|
+
}
|
|
159
|
+
this._closeBtn.setAttribute('aria-label', this.getAttribute('close-label') || 'Close');
|
|
160
|
+
this._syncInert();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_syncInert() {
|
|
164
|
+
const hidden = !this.getBoolAttr('visible');
|
|
165
|
+
this._actionBtn.inert = hidden;
|
|
166
|
+
this._closeBtn.inert = hidden;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Effective duration: the attribute when set; otherwise none for a toast
|
|
170
|
+
// with an action, the default for a plain one.
|
|
171
|
+
_duration() {
|
|
172
|
+
const attr = this.getAttribute('duration');
|
|
173
|
+
if (attr !== null && attr.trim() !== '' && Number.isFinite(Number(attr))) return Number(attr);
|
|
174
|
+
return this.getAttribute('action-label') ? 0 : DEFAULT_DURATION;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_arm() {
|
|
178
|
+
clearTimeout(this._timer);
|
|
179
|
+
const duration = this._duration();
|
|
180
|
+
if (duration > 0 && !this._held && this.getBoolAttr('visible')) {
|
|
74
181
|
this._timer = setTimeout(() => {
|
|
75
182
|
this.removeAttribute('visible');
|
|
76
183
|
this.emit('glk-dismiss');
|
|
@@ -78,27 +185,84 @@ class GlkToast extends GlkElement {
|
|
|
78
185
|
}
|
|
79
186
|
}
|
|
80
187
|
|
|
188
|
+
_show() {
|
|
189
|
+
this._toast.classList.add('is-visible');
|
|
190
|
+
this._syncInert();
|
|
191
|
+
this._arm();
|
|
192
|
+
}
|
|
193
|
+
|
|
81
194
|
_hide() {
|
|
82
195
|
this._toast.classList.remove('is-visible');
|
|
83
196
|
clearTimeout(this._timer);
|
|
197
|
+
this._held = false;
|
|
198
|
+
// A hidden toast must not keep the focus: hand it back, else let it go.
|
|
199
|
+
if (this._toast.contains(this._shadow.activeElement)) {
|
|
200
|
+
const back = this._returnFocus;
|
|
201
|
+
if (back?.isConnected) back.focus({ preventScroll: true });
|
|
202
|
+
if (this._toast.contains(this._shadow.activeElement)) this._shadow.activeElement.blur();
|
|
203
|
+
}
|
|
204
|
+
this._returnFocus = null;
|
|
205
|
+
this._syncInert();
|
|
84
206
|
}
|
|
85
207
|
|
|
86
|
-
|
|
87
|
-
|
|
208
|
+
_close() {
|
|
209
|
+
this.removeAttribute('visible');
|
|
210
|
+
this.emit('glk-close');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_takeAction() {
|
|
214
|
+
const label = this.getAttribute('action-label') || '';
|
|
215
|
+
const action = this.getAttribute('action-value') ?? label;
|
|
216
|
+
const shown = this._shown;
|
|
217
|
+
// A listener that shows the next message ("Restored") keeps the toast up,
|
|
218
|
+
// and so does one that calls preventDefault().
|
|
219
|
+
if (this.emit('glk-action', { action, label }, { cancelable: true }) && this._shown === shown) {
|
|
220
|
+
this.removeAttribute('visible');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* show(message, variant, duration) — as before.
|
|
226
|
+
* show(message, { variant, duration, action: { label, value }, dismissible })
|
|
227
|
+
* — keys that are left out keep their attribute, except action: a message
|
|
228
|
+
* without one shows no button, and one with an action but no duration stays.
|
|
229
|
+
*/
|
|
230
|
+
show(message, variantOrOptions, duration) {
|
|
231
|
+
this._shown = (this._shown ?? 0) + 1;
|
|
88
232
|
if (message) this.setAttribute('message', message);
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
233
|
+
if (variantOrOptions && typeof variantOrOptions === 'object') {
|
|
234
|
+
const o = variantOrOptions;
|
|
235
|
+
if ('variant' in o) this._setOrRemove('variant', o.variant);
|
|
236
|
+
if (o.action) {
|
|
237
|
+
this.setAttribute('action-label', String(o.action.label ?? o.action.value ?? ''));
|
|
238
|
+
this._setOrRemove('action-value', o.action.value);
|
|
239
|
+
} else {
|
|
240
|
+
this.removeAttribute('action-label');
|
|
241
|
+
this.removeAttribute('action-value');
|
|
242
|
+
}
|
|
243
|
+
if ('duration' in o) this._setOrRemove('duration', o.duration);
|
|
244
|
+
else if (o.action) this.removeAttribute('duration');
|
|
245
|
+
if ('dismissible' in o) this.setBoolAttr('dismissible', o.dismissible);
|
|
246
|
+
} else {
|
|
247
|
+
if (variantOrOptions) this.setAttribute('variant', variantOrOptions);
|
|
248
|
+
if (duration) this.setAttribute('duration', String(duration));
|
|
249
|
+
}
|
|
250
|
+
// Already visible: the attribute does not change, so restart by hand.
|
|
251
|
+
if (this.getBoolAttr('visible')) this._show();
|
|
252
|
+
else this.setAttribute('visible', '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_setOrRemove(name, value) {
|
|
256
|
+
if (value == null || value === '') this.removeAttribute(name);
|
|
257
|
+
else this.setAttribute(name, String(value));
|
|
92
258
|
}
|
|
93
259
|
|
|
94
260
|
dismiss() {
|
|
95
261
|
this.removeAttribute('visible');
|
|
96
262
|
}
|
|
97
263
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
clearTimeout(this._timer);
|
|
101
|
-
}
|
|
264
|
+
get dismissible() { return this.getBoolAttr('dismissible'); }
|
|
265
|
+
set dismissible(v) { this.setBoolAttr('dismissible', v); }
|
|
102
266
|
}
|
|
103
267
|
|
|
104
268
|
customElements.define('glk-toast', GlkToast);
|