@jungherz-de/glasskit-elements 1.27.0 → 1.28.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/CHANGELOG.md +31 -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 +40 -5
- 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 +158 -37
- package/dist/glasskit-elements.js +158 -37
- 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 +39 -4
- 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.0",
|
|
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
|
}
|
|
@@ -121,14 +148,18 @@ class GlkSegmented extends GlkFormElement {
|
|
|
121
148
|
this.emit('glk-change', { value });
|
|
122
149
|
};
|
|
123
150
|
this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
|
|
151
|
+
this._onSlotChange = () => this._syncIcons();
|
|
124
152
|
this._group.addEventListener('click', this._onClick);
|
|
153
|
+
this._group.addEventListener('slotchange', this._onSlotChange);
|
|
125
154
|
this._resizeObserver = new ResizeObserver(this._onResize);
|
|
126
155
|
this._resizeObserver.observe(this._group);
|
|
156
|
+
this._syncIcons();
|
|
127
157
|
this._applyDroplet();
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
teardownEvents() {
|
|
131
161
|
this._group?.removeEventListener('click', this._onClick);
|
|
162
|
+
this._group?.removeEventListener('slotchange', this._onSlotChange);
|
|
132
163
|
this._resizeObserver?.disconnect();
|
|
133
164
|
this._resizeObserver = null;
|
|
134
165
|
this._droplet?.destroy();
|
|
@@ -151,6 +182,7 @@ class GlkSegmented extends GlkFormElement {
|
|
|
151
182
|
case 'full':
|
|
152
183
|
case 'overflow': this._applyLayout(); break;
|
|
153
184
|
case 'droplet': this._applyLayout(); this._applyDroplet(); break;
|
|
185
|
+
case 'icon-only': this._build(); this._applyLayout(); break;
|
|
154
186
|
case 'label': this._applyLabel(); break;
|
|
155
187
|
}
|
|
156
188
|
}
|
|
@@ -173,6 +205,9 @@ class GlkSegmented extends GlkFormElement {
|
|
|
173
205
|
get droplet() { return this.getBoolAttr('droplet'); }
|
|
174
206
|
set droplet(v) { this.setBoolAttr('droplet', v); }
|
|
175
207
|
|
|
208
|
+
get iconOnly() { return this.getBoolAttr('icon-only'); }
|
|
209
|
+
set iconOnly(v) { this.setBoolAttr('icon-only', v); }
|
|
210
|
+
|
|
176
211
|
get overflow() {
|
|
177
212
|
const v = this.getAttribute('overflow');
|
|
178
213
|
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();
|