@jungherz-de/glasskit-elements 1.17.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 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.17.0-lightgrey?style=flat-square" alt="Changelog"></a>
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.17/glasskit.min.css">
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.17/glasskit.min.css">
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>
@@ -137,7 +137,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
137
137
  | Component | Description | Key Attributes |
138
138
  |---|---|---|
139
139
  | `<glk-card>` | Glass-effect content card | `glow`, `fill` |
140
- | `<glk-badge>` | Inline status badge, or a filter chip with `interactive` | `variant` (primary, success, error), `interactive`, `selected` |
140
+ | `<glk-badge>` | Inline status badge, or a filter chip with `interactive` | `variant` (primary, success, warning, error), `interactive`, `selected` |
141
141
  | `<glk-avatar>` | Circular avatar | `size` (sm, lg), `src` |
142
142
  | `<glk-title>` | Styled heading | — |
143
143
  | `<glk-divider>` | Horizontal divider | — |
@@ -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>` | Auto-dismissing notification | `message`, `variant` (success, error, warning), `duration` |
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
 
@@ -220,10 +220,15 @@ class GlkElement extends HTMLElement {
220
220
  return el;
221
221
  }
222
222
 
223
- emit(eventName, detail = null) {
224
- this.dispatchEvent(new CustomEvent(eventName, {
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,7 +1,9 @@
1
1
  import { GlkElement } from './base.js';
2
2
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
- const VARIANTS = ['primary', 'success', 'error'];
4
+ // warning since 1.18.0 (needs GlassKit 1.18.0). Any other value falls back to
5
+ // the neutral badge, as an unknown value of an enumerated attribute does.
6
+ const VARIANTS = ['primary', 'success', 'warning', 'error'];
5
7
 
6
8
  class GlkBadge extends GlkElement {
7
9
  static get displayInline() { return true; }
@@ -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: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`,
8
- error: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><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>`,
9
- warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><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>`
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
- this._show();
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
- this._show();
48
- } else {
49
- this._hide();
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
- classes.push(`glass-toast--${variant}`);
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
- const variant = this.getAttribute('variant');
66
- this._iconEl.innerHTML = ICONS[variant] || ICONS.success;
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
- _show() {
70
- this._toast.classList.add('is-visible');
71
- const duration = parseInt(this.getAttribute('duration') || '3000', 10);
72
- if (duration > 0) {
73
- clearTimeout(this._timer);
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
- /** Programmatic show */
87
- show(message, variant, duration) {
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 (variant) this.setAttribute('variant', variant);
90
- if (duration) this.setAttribute('duration', String(duration));
91
- this.setAttribute('visible', '');
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
- disconnectedCallback() {
99
- super.disconnectedCallback();
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);