@jungherz-de/glasskit-elements 1.27.0 → 1.28.1
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/CHANGELOG.md +42 -1
- package/README.md +26 -5
- package/SKILL.md +47 -10
- package/dist/components/base.js +9 -3
- package/dist/components/glk-modal.js +37 -1
- package/dist/components/glk-segmented.js +44 -6
- package/dist/components/glk-tab-bar.js +22 -15
- package/dist/components/glk-tab-item.js +20 -3
- package/dist/components/shared/{glasskit-droplet-C3k1mxn4.js → glasskit-droplet-3vJVXB3u.js} +30 -10
- package/dist/glasskit-elements.esm.js +162 -38
- package/dist/glasskit-elements.js +162 -38
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +9 -3
- package/src/components/feedback/glk-modal.js +37 -1
- package/src/components/forms/glk-segmented.js +43 -5
- package/src/components/navigation/glk-tab-bar.js +21 -14
- package/src/components/navigation/glk-tab-item.js +20 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/glasskit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.1",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the GlassKit CSS glassmorphism library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://glasskit-elements.jungherz.com",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"vanilla-js"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@jungherz-de/glasskit": ">=1.
|
|
52
|
+
"@jungherz-de/glasskit": ">=1.28.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@jungherz-de/glasskit": "^1.
|
|
55
|
+
"@jungherz-de/glasskit": "^1.28.0",
|
|
56
56
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
57
57
|
"@rollup/plugin-terser": "^0.4.0",
|
|
58
58
|
"rollup": "^4.0.0"
|
package/src/base.js
CHANGED
|
@@ -209,8 +209,9 @@ export class GlkElement extends HTMLElement {
|
|
|
209
209
|
/**
|
|
210
210
|
* Opt in when the component copies light-DOM children into its shadow tree.
|
|
211
211
|
* A MutationObserver then calls projectLightDom() again whenever those
|
|
212
|
-
* children change
|
|
213
|
-
*
|
|
212
|
+
* children change — their content and, since 1.28.0, their attributes — so
|
|
213
|
+
* a framework that swaps them or only binds `disabled` keeps the rendered
|
|
214
|
+
* element in step. Without it the copy is made once and silently goes stale.
|
|
214
215
|
*/
|
|
215
216
|
static get observesLightDom() { return false; }
|
|
216
217
|
|
|
@@ -272,8 +273,13 @@ export class GlkElement extends HTMLElement {
|
|
|
272
273
|
|
|
273
274
|
if (this.constructor.observesLightDom) {
|
|
274
275
|
this._lightDomObserver ??= new MutationObserver(records => this.projectLightDom(records));
|
|
276
|
+
// Attributes too: the copies show them. Until 1.27.0 a modal action
|
|
277
|
+
// that started disabled stayed disabled in its copy after the page
|
|
278
|
+
// enabled it, and the dialog could not be confirmed. The elements'
|
|
279
|
+
// own projectLightDom() compare a signature first, so a change
|
|
280
|
+
// elsewhere in the content costs a string comparison.
|
|
275
281
|
this._lightDomObserver.observe(this, {
|
|
276
|
-
childList: true, subtree: true, characterData: true
|
|
282
|
+
childList: true, subtree: true, characterData: true, attributes: true
|
|
277
283
|
});
|
|
278
284
|
}
|
|
279
285
|
}
|
|
@@ -23,6 +23,33 @@ function reducedMotion() {
|
|
|
23
23
|
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// The copies carry no inline handlers (onclick="…"): a click on a copy is
|
|
27
|
+
// handed to its original, and until 1.28.0 such a handler ran twice.
|
|
28
|
+
const isHandler = name => name.startsWith('on');
|
|
29
|
+
|
|
30
|
+
function copyOf(node) {
|
|
31
|
+
const copy = node.cloneNode(true);
|
|
32
|
+
if (copy.nodeType === Node.ELEMENT_NODE) {
|
|
33
|
+
for (const el of [copy, ...copy.querySelectorAll('*')]) {
|
|
34
|
+
for (const { name } of [...el.attributes]) if (isHandler(name)) el.removeAttribute(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return copy;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Gives an action's copy the attributes and the content of its original. */
|
|
41
|
+
function mirror(copy, original) {
|
|
42
|
+
for (const { name } of [...copy.attributes]) {
|
|
43
|
+
if (!original.hasAttribute(name)) copy.removeAttribute(name);
|
|
44
|
+
}
|
|
45
|
+
for (const { name, value } of original.attributes) {
|
|
46
|
+
if (!isHandler(name) && copy.getAttribute(name) !== value) copy.setAttribute(name, value);
|
|
47
|
+
}
|
|
48
|
+
if (copy.innerHTML !== original.innerHTML) {
|
|
49
|
+
copy.replaceChildren(...[...original.childNodes].map(copyOf));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
class GlkModal extends GlkElement {
|
|
27
54
|
static get observedAttributes() {
|
|
28
55
|
return ['open', 'title'];
|
|
@@ -68,9 +95,18 @@ class GlkModal extends GlkElement {
|
|
|
68
95
|
if (signature === this._footerSignature) return;
|
|
69
96
|
this._footerSignature = signature;
|
|
70
97
|
|
|
98
|
+
// As many actions as copies: each copy takes its original's attributes
|
|
99
|
+
// and content in place, so the one that has the focus keeps it — a
|
|
100
|
+
// confirm button that the page enables, or relabels while it works.
|
|
101
|
+
const copies = [...this._footer.children];
|
|
102
|
+
if (copies.length === buttons.length) {
|
|
103
|
+
buttons.forEach((btn, i) => mirror(copies[i], btn));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
this._footer.innerHTML = '';
|
|
72
108
|
buttons.forEach((btn, i) => {
|
|
73
|
-
const clone = btn
|
|
109
|
+
const clone = copyOf(btn);
|
|
74
110
|
// Look the original up again at click time instead of closing over it: a
|
|
75
111
|
// framework that re-renders replaces the node, and a captured reference
|
|
76
112
|
// would then forward the click to a button no longer in the document.
|
|
@@ -12,9 +12,16 @@ import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
|
|
|
12
12
|
// disabled (the whole row; since 1.22.3, as is a disabled
|
|
13
13
|
// fieldset around it), droplet (since 1.27.0: the chosen option
|
|
14
14
|
// sits on a drop of glass that glides to the next one and can be
|
|
15
|
-
// dragged — GlassKit's .glass-segmented--droplet)
|
|
15
|
+
// dragged — GlassKit's .glass-segmented--droplet), icon-only
|
|
16
|
+
// (since 1.28.0: the options show their icons alone, each label
|
|
17
|
+
// becomes the button's aria-label and title — GlassKit's
|
|
18
|
+
// .glass-segmented--icons)
|
|
16
19
|
// Properties: value, options (array or JSON text), full, overflow, label,
|
|
17
|
-
// disabled, droplet
|
|
20
|
+
// disabled, droplet, iconOnly
|
|
21
|
+
// Slots: icon-<value> (since 1.28.0) — the icon of the option with that
|
|
22
|
+
// value: an <svg>, a font icon, an <img>. It sits in GlassKit's
|
|
23
|
+
// .glass-segmented__icon before the label and takes the option's
|
|
24
|
+
// colour; an option without one has no icon box.
|
|
18
25
|
// Event: glk-change { value } — only on a change made by the user
|
|
19
26
|
// Part: group
|
|
20
27
|
//
|
|
@@ -43,7 +50,7 @@ function parseList(json) {
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
class GlkSegmented extends GlkFormElement {
|
|
46
|
-
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet']; }
|
|
53
|
+
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet', 'icon-only']; }
|
|
47
54
|
|
|
48
55
|
// A row of buttons with one tab stop: delegated focus would land on the
|
|
49
56
|
// first button, not the chosen one.
|
|
@@ -60,20 +67,39 @@ class GlkSegmented extends GlkFormElement {
|
|
|
60
67
|
|
|
61
68
|
_build() {
|
|
62
69
|
this._group.replaceChildren();
|
|
70
|
+
const iconOnly = this.iconOnly;
|
|
63
71
|
for (const option of parseList(this.getAttribute('options'))) {
|
|
64
72
|
const value = String(option.value ?? '');
|
|
73
|
+
const label = String(option.label ?? value);
|
|
65
74
|
const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
|
|
66
75
|
if (TONES.includes(option.tone)) {
|
|
67
76
|
button.classList.add(`glass-segmented__item--${option.tone}`);
|
|
68
77
|
button.appendChild(this.createElement('span', ['glass-segmented__dot']));
|
|
69
78
|
}
|
|
70
|
-
|
|
79
|
+
// The label names the button; an icon font's text must not join it.
|
|
80
|
+
const icon = this.createElement('span', ['glass-segmented__icon'], { 'aria-hidden': 'true' });
|
|
81
|
+
icon.appendChild(this.createElement('slot', [], { name: `icon-${value}` }));
|
|
82
|
+
button.appendChild(icon);
|
|
83
|
+
if (iconOnly) {
|
|
84
|
+
button.setAttribute('aria-label', label);
|
|
85
|
+
button.title = label;
|
|
86
|
+
} else {
|
|
87
|
+
button.appendChild(document.createTextNode(label));
|
|
88
|
+
}
|
|
71
89
|
this._group.appendChild(button);
|
|
72
90
|
}
|
|
91
|
+
this._syncIcons();
|
|
73
92
|
this.applyDisabled(this._actuallyDisabled);
|
|
74
93
|
this._syncValue();
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
/** An option shows its icon box only while an icon is slotted into it. */
|
|
97
|
+
_syncIcons() {
|
|
98
|
+
for (const slot of this._group.querySelectorAll('.glass-segmented__icon > slot')) {
|
|
99
|
+
slot.parentElement.hidden = slot.assignedElements().length === 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
/** The whole row is disabled, or an option by its own `disabled`. */
|
|
78
104
|
applyDisabled(disabled) {
|
|
79
105
|
const options = this.options;
|
|
@@ -95,6 +121,7 @@ class GlkSegmented extends GlkFormElement {
|
|
|
95
121
|
const overflow = this.overflow;
|
|
96
122
|
this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
|
|
97
123
|
this._group.classList.toggle('glass-segmented--droplet', this.getBoolAttr('droplet'));
|
|
124
|
+
this._group.classList.toggle('glass-segmented--icons', this.iconOnly);
|
|
98
125
|
for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
|
|
99
126
|
this._reveal('instant');
|
|
100
127
|
}
|
|
@@ -113,7 +140,10 @@ class GlkSegmented extends GlkFormElement {
|
|
|
113
140
|
|
|
114
141
|
setupEvents() {
|
|
115
142
|
this._onClick = (event) => {
|
|
116
|
-
|
|
143
|
+
// The option's button from the event's path. A click on a slotted icon
|
|
144
|
+
// has that icon as its target, in the light DOM, where closest() never
|
|
145
|
+
// meets the button — until 1.28.1 such a tap chose nothing.
|
|
146
|
+
const button = event.composedPath().find(node => node.parentNode === this._group);
|
|
117
147
|
if (!button || button.disabled) return;
|
|
118
148
|
const { value } = button.dataset;
|
|
119
149
|
if (value === this.value) return;
|
|
@@ -121,14 +151,18 @@ class GlkSegmented extends GlkFormElement {
|
|
|
121
151
|
this.emit('glk-change', { value });
|
|
122
152
|
};
|
|
123
153
|
this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
|
|
154
|
+
this._onSlotChange = () => this._syncIcons();
|
|
124
155
|
this._group.addEventListener('click', this._onClick);
|
|
156
|
+
this._group.addEventListener('slotchange', this._onSlotChange);
|
|
125
157
|
this._resizeObserver = new ResizeObserver(this._onResize);
|
|
126
158
|
this._resizeObserver.observe(this._group);
|
|
159
|
+
this._syncIcons();
|
|
127
160
|
this._applyDroplet();
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
teardownEvents() {
|
|
131
164
|
this._group?.removeEventListener('click', this._onClick);
|
|
165
|
+
this._group?.removeEventListener('slotchange', this._onSlotChange);
|
|
132
166
|
this._resizeObserver?.disconnect();
|
|
133
167
|
this._resizeObserver = null;
|
|
134
168
|
this._droplet?.destroy();
|
|
@@ -151,6 +185,7 @@ class GlkSegmented extends GlkFormElement {
|
|
|
151
185
|
case 'full':
|
|
152
186
|
case 'overflow': this._applyLayout(); break;
|
|
153
187
|
case 'droplet': this._applyLayout(); this._applyDroplet(); break;
|
|
188
|
+
case 'icon-only': this._build(); this._applyLayout(); break;
|
|
154
189
|
case 'label': this._applyLabel(); break;
|
|
155
190
|
}
|
|
156
191
|
}
|
|
@@ -173,6 +208,9 @@ class GlkSegmented extends GlkFormElement {
|
|
|
173
208
|
get droplet() { return this.getBoolAttr('droplet'); }
|
|
174
209
|
set droplet(v) { this.setBoolAttr('droplet', v); }
|
|
175
210
|
|
|
211
|
+
get iconOnly() { return this.getBoolAttr('icon-only'); }
|
|
212
|
+
set iconOnly(v) { this.setBoolAttr('icon-only', v); }
|
|
213
|
+
|
|
176
214
|
get overflow() {
|
|
177
215
|
const v = this.getAttribute('overflow');
|
|
178
216
|
return OVERFLOWS.includes(v) ? v : '';
|
|
@@ -82,20 +82,20 @@ class GlkTabBar extends GlkElement {
|
|
|
82
82
|
this._syncItems();
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.addEventListener('glk-tab-select', this._onTabSelect);
|
|
85
|
+
/**
|
|
86
|
+
* Called by an item the user selected once its glk-tab-select went through
|
|
87
|
+
* uncancelled; the item is active already. Until 1.27.0 the bar did this
|
|
88
|
+
* while the event was still on its way, so a listener further up could not
|
|
89
|
+
* have kept the current tab.
|
|
90
|
+
*/
|
|
91
|
+
_selected(tab) {
|
|
92
|
+
for (const item of this.querySelectorAll('glk-tab-item')) {
|
|
93
|
+
if (item !== tab) item.removeAttribute('active');
|
|
94
|
+
}
|
|
95
|
+
this.emit('glk-tab-change', { tab });
|
|
96
|
+
}
|
|
98
97
|
|
|
98
|
+
setupEvents() {
|
|
99
99
|
// The drop's state lives on the nav; the items carry it on their wrappers.
|
|
100
100
|
this._navObserver = new MutationObserver(() => this._syncItems());
|
|
101
101
|
this._navObserver.observe(this._nav, { attributes: true, attributeFilter: ['data-droplet', 'data-droplet-drag'] });
|
|
@@ -106,12 +106,19 @@ class GlkTabBar extends GlkElement {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
teardownEvents() {
|
|
109
|
-
this.removeEventListener('glk-tab-select', this._onTabSelect);
|
|
110
109
|
this._navObserver?.disconnect();
|
|
111
110
|
this._itemObserver?.disconnect();
|
|
112
111
|
this._droplet?.destroy();
|
|
113
112
|
this._droplet = null;
|
|
114
113
|
}
|
|
114
|
+
|
|
115
|
+
// Properties as well (since 1.28.0): `bar.droplet = true` set an own
|
|
116
|
+
// property before and changed nothing.
|
|
117
|
+
get floating() { return this.getBoolAttr('floating'); }
|
|
118
|
+
set floating(v) { this.setBoolAttr('floating', v); }
|
|
119
|
+
|
|
120
|
+
get droplet() { return this.getBoolAttr('droplet'); }
|
|
121
|
+
set droplet(v) { this.setBoolAttr('droplet', v); }
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
customElements.define('glk-tab-bar', GlkTabBar);
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { GlkElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
+
// The icon: an inline <svg> child is copied into the shadow root, where
|
|
4
|
+
// GlassKit's rules reach it. Anything else — a font icon such as Material
|
|
5
|
+
// Icons, an <img>, an element of the page's own — goes into slot="icon"
|
|
6
|
+
// (since 1.28.0) and takes the state colour from GlassKit's icon box: active,
|
|
7
|
+
// hover, the lens of the drop and the drag. With a slotted icon no SVG is
|
|
8
|
+
// copied.
|
|
9
|
+
//
|
|
10
|
+
// glk-tab-select (cancelable since 1.28.0) comes before `active` changes:
|
|
11
|
+
// preventDefault() keeps the current tab — for an item that opens a drawer
|
|
12
|
+
// or a link — and the bar then neither moves `active` nor its drop, nor
|
|
13
|
+
// emits glk-tab-change. A drag of the drop that ends on the item clicks it,
|
|
14
|
+
// so it takes the same path.
|
|
15
|
+
|
|
3
16
|
class GlkTabItem extends GlkElement {
|
|
4
17
|
static get observedAttributes() {
|
|
5
18
|
return ['label', 'active', 'badge'];
|
|
@@ -11,8 +24,10 @@ class GlkTabItem extends GlkElement {
|
|
|
11
24
|
this._btn = this.createElement('button', ['glass-tab-bar__item']);
|
|
12
25
|
if (this.getBoolAttr('active')) this._btn.classList.add('is-active');
|
|
13
26
|
|
|
14
|
-
// Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies
|
|
27
|
+
// Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies,
|
|
28
|
+
// or show what is slotted as icon
|
|
15
29
|
this._iconEl = this.createElement('span', ['glass-tab-bar__icon']);
|
|
30
|
+
this._iconEl.appendChild(this.createElement('slot', [], { name: 'icon' }));
|
|
16
31
|
|
|
17
32
|
// Label
|
|
18
33
|
this._labelEl = this.createElement('span', ['glass-tab-bar__label']);
|
|
@@ -36,7 +51,8 @@ class GlkTabItem extends GlkElement {
|
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
projectLightDom() {
|
|
39
|
-
|
|
54
|
+
// A slotted icon wins; only without one is an <svg> copied.
|
|
55
|
+
const svg = this.querySelector(':scope > [slot="icon"]') ? null : this.querySelector('svg');
|
|
40
56
|
const signature = svg ? svg.outerHTML : '';
|
|
41
57
|
if (signature === this._iconSignature) return;
|
|
42
58
|
this._iconSignature = signature;
|
|
@@ -62,8 +78,9 @@ class GlkTabItem extends GlkElement {
|
|
|
62
78
|
|
|
63
79
|
setupEvents() {
|
|
64
80
|
this._onClick = () => {
|
|
81
|
+
if (!this.emit('glk-tab-select', null, { cancelable: true })) return;
|
|
65
82
|
this.setAttribute('active', '');
|
|
66
|
-
this.
|
|
83
|
+
this.closest('glk-tab-bar')?._selected?.(this);
|
|
67
84
|
};
|
|
68
85
|
this._btn.addEventListener('click', this._onClick);
|
|
69
86
|
this._tellBar();
|