@jungherz-de/glasskit-elements 1.26.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 +47 -1
- package/README.md +49 -6
- package/SKILL.md +72 -10
- package/dist/components/base.js +9 -3
- package/dist/components/glk-modal.js +37 -1
- package/dist/components/glk-segmented.js +58 -4
- package/dist/components/glk-tab-bar.js +81 -14
- package/dist/components/glk-tab-item.js +42 -3
- package/dist/components/shared/glasskit-droplet-3vJVXB3u.js +237 -0
- package/dist/glasskit-elements.esm.js +464 -28
- package/dist/glasskit-elements.js +464 -28
- 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 +58 -4
- package/src/components/navigation/glk-tab-bar.js +81 -14
- package/src/components/navigation/glk-tab-item.js +42 -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.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GlkFormElement } from '../../base.js';
|
|
2
2
|
import { revealCentered } from '../../reveal.js';
|
|
3
|
+
import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
|
|
3
4
|
|
|
4
5
|
// One button per option, the chosen one marked aria-pressed="true" — GlassKit
|
|
5
6
|
// styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
|
|
@@ -9,9 +10,18 @@ import { revealCentered } from '../../reveal.js';
|
|
|
9
10
|
// do not fit — without it the row stays one line and runs past
|
|
10
11
|
// the edge), label (aria-label of the group), name (form field),
|
|
11
12
|
// disabled (the whole row; since 1.22.3, as is a disabled
|
|
12
|
-
// fieldset around it)
|
|
13
|
+
// fieldset around it), droplet (since 1.27.0: the chosen option
|
|
14
|
+
// sits on a drop of glass that glides to the next one and can be
|
|
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)
|
|
13
19
|
// Properties: value, options (array or JSON text), full, overflow, label,
|
|
14
|
-
// disabled
|
|
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.
|
|
15
25
|
// Event: glk-change { value } — only on a change made by the user
|
|
16
26
|
// Part: group
|
|
17
27
|
//
|
|
@@ -40,7 +50,7 @@ function parseList(json) {
|
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
class GlkSegmented extends GlkFormElement {
|
|
43
|
-
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
|
|
53
|
+
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet', 'icon-only']; }
|
|
44
54
|
|
|
45
55
|
// A row of buttons with one tab stop: delegated focus would land on the
|
|
46
56
|
// first button, not the chosen one.
|
|
@@ -57,20 +67,39 @@ class GlkSegmented extends GlkFormElement {
|
|
|
57
67
|
|
|
58
68
|
_build() {
|
|
59
69
|
this._group.replaceChildren();
|
|
70
|
+
const iconOnly = this.iconOnly;
|
|
60
71
|
for (const option of parseList(this.getAttribute('options'))) {
|
|
61
72
|
const value = String(option.value ?? '');
|
|
73
|
+
const label = String(option.label ?? value);
|
|
62
74
|
const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
|
|
63
75
|
if (TONES.includes(option.tone)) {
|
|
64
76
|
button.classList.add(`glass-segmented__item--${option.tone}`);
|
|
65
77
|
button.appendChild(this.createElement('span', ['glass-segmented__dot']));
|
|
66
78
|
}
|
|
67
|
-
|
|
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
|
+
}
|
|
68
89
|
this._group.appendChild(button);
|
|
69
90
|
}
|
|
91
|
+
this._syncIcons();
|
|
70
92
|
this.applyDisabled(this._actuallyDisabled);
|
|
71
93
|
this._syncValue();
|
|
72
94
|
}
|
|
73
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
|
+
|
|
74
103
|
/** The whole row is disabled, or an option by its own `disabled`. */
|
|
75
104
|
applyDisabled(disabled) {
|
|
76
105
|
const options = this.options;
|
|
@@ -91,6 +120,8 @@ class GlkSegmented extends GlkFormElement {
|
|
|
91
120
|
_applyLayout() {
|
|
92
121
|
const overflow = this.overflow;
|
|
93
122
|
this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
|
|
123
|
+
this._group.classList.toggle('glass-segmented--droplet', this.getBoolAttr('droplet'));
|
|
124
|
+
this._group.classList.toggle('glass-segmented--icons', this.iconOnly);
|
|
94
125
|
for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
|
|
95
126
|
this._reveal('instant');
|
|
96
127
|
}
|
|
@@ -117,15 +148,30 @@ class GlkSegmented extends GlkFormElement {
|
|
|
117
148
|
this.emit('glk-change', { value });
|
|
118
149
|
};
|
|
119
150
|
this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
|
|
151
|
+
this._onSlotChange = () => this._syncIcons();
|
|
120
152
|
this._group.addEventListener('click', this._onClick);
|
|
153
|
+
this._group.addEventListener('slotchange', this._onSlotChange);
|
|
121
154
|
this._resizeObserver = new ResizeObserver(this._onResize);
|
|
122
155
|
this._resizeObserver.observe(this._group);
|
|
156
|
+
this._syncIcons();
|
|
157
|
+
this._applyDroplet();
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
teardownEvents() {
|
|
126
161
|
this._group?.removeEventListener('click', this._onClick);
|
|
162
|
+
this._group?.removeEventListener('slotchange', this._onSlotChange);
|
|
127
163
|
this._resizeObserver?.disconnect();
|
|
128
164
|
this._resizeObserver = null;
|
|
165
|
+
this._droplet?.destroy();
|
|
166
|
+
this._droplet = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The drop follows aria-pressed inside this shadow root by itself; a drag
|
|
170
|
+
// that ends on an option clicks it, the same path as a tap.
|
|
171
|
+
_applyDroplet() {
|
|
172
|
+
const on = this.getBoolAttr('droplet') && this.isConnected;
|
|
173
|
+
if (on && !this._droplet) this._droplet = attachDroplet(this._group);
|
|
174
|
+
else if (!on && this._droplet) { this._droplet.destroy(); this._droplet = null; }
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
onAttributeChanged(name) {
|
|
@@ -135,6 +181,8 @@ class GlkSegmented extends GlkFormElement {
|
|
|
135
181
|
case 'options': this._build(); break;
|
|
136
182
|
case 'full':
|
|
137
183
|
case 'overflow': this._applyLayout(); break;
|
|
184
|
+
case 'droplet': this._applyLayout(); this._applyDroplet(); break;
|
|
185
|
+
case 'icon-only': this._build(); this._applyLayout(); break;
|
|
138
186
|
case 'label': this._applyLabel(); break;
|
|
139
187
|
}
|
|
140
188
|
}
|
|
@@ -154,6 +202,12 @@ class GlkSegmented extends GlkFormElement {
|
|
|
154
202
|
get full() { return this.getBoolAttr('full'); }
|
|
155
203
|
set full(v) { this.setBoolAttr('full', v); }
|
|
156
204
|
|
|
205
|
+
get droplet() { return this.getBoolAttr('droplet'); }
|
|
206
|
+
set droplet(v) { this.setBoolAttr('droplet', v); }
|
|
207
|
+
|
|
208
|
+
get iconOnly() { return this.getBoolAttr('icon-only'); }
|
|
209
|
+
set iconOnly(v) { this.setBoolAttr('icon-only', v); }
|
|
210
|
+
|
|
157
211
|
get overflow() {
|
|
158
212
|
const v = this.getAttribute('overflow');
|
|
159
213
|
return OVERFLOWS.includes(v) ? v : '';
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { GlkElement } from '../../base.js';
|
|
2
|
+
import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
|
|
3
|
+
|
|
4
|
+
// droplet (since 1.27.0): the active item sits on a drop of glass that glides
|
|
5
|
+
// to the next one and can be dragged — GlassKit's .glass-tab-bar--droplet,
|
|
6
|
+
// moved by its glasskit-droplet.js. The items live in shadow roots of their
|
|
7
|
+
// own, so the bar hands its state down to them (see glk-tab-item _syncBar)
|
|
8
|
+
// and tells the drop when the active item changed.
|
|
2
9
|
|
|
3
10
|
class GlkTabBar extends GlkElement {
|
|
4
11
|
static get observedAttributes() {
|
|
5
|
-
return ['static', 'floating'];
|
|
12
|
+
return ['static', 'floating', 'droplet'];
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
render() {
|
|
@@ -14,6 +21,7 @@ class GlkTabBar extends GlkElement {
|
|
|
14
21
|
if (this.getBoolAttr('floating')) {
|
|
15
22
|
nav.classList.add('glass-tab-bar--floating');
|
|
16
23
|
}
|
|
24
|
+
nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
|
|
17
25
|
|
|
18
26
|
if (this.getBoolAttr('static')) {
|
|
19
27
|
this._applyStatic();
|
|
@@ -26,6 +34,10 @@ class GlkTabBar extends GlkElement {
|
|
|
26
34
|
this._applyStatic();
|
|
27
35
|
} else if (name === 'floating') {
|
|
28
36
|
this._nav.classList.toggle('glass-tab-bar--floating', this.getBoolAttr('floating'));
|
|
37
|
+
this._syncItems();
|
|
38
|
+
} else if (name === 'droplet') {
|
|
39
|
+
this._nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
|
|
40
|
+
this._applyDroplet();
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -34,24 +46,79 @@ class GlkTabBar extends GlkElement {
|
|
|
34
46
|
this._nav.style.borderRadius = '16px';
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.
|
|
49
|
+
_items() {
|
|
50
|
+
return [...this.children].filter(el => el.localName === 'glk-tab-item');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Hands the bar's floating and droplet state to every item's wrapper. */
|
|
54
|
+
_syncItems() {
|
|
55
|
+
const state = {
|
|
56
|
+
floating: this.getBoolAttr('floating'),
|
|
57
|
+
droplet: this.getBoolAttr('droplet'),
|
|
58
|
+
placed: Boolean(this._nav?.hasAttribute('data-droplet')),
|
|
59
|
+
drag: Boolean(this._nav?.hasAttribute('data-droplet-drag')),
|
|
48
60
|
};
|
|
49
|
-
this.
|
|
61
|
+
for (const item of this._items()) item._syncBar?.(state);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Called by an item that rendered or changed its active state. */
|
|
65
|
+
_itemChanged() {
|
|
66
|
+
this._syncItems();
|
|
67
|
+
this._droplet?.update();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_applyDroplet() {
|
|
71
|
+
const on = this.getBoolAttr('droplet') && this.isConnected;
|
|
72
|
+
if (on && !this._droplet) {
|
|
73
|
+
// The drop measures and marks the items' own buttons; picking one clicks
|
|
74
|
+
// it, which is what a tap does.
|
|
75
|
+
this._droplet = attachDroplet(this._nav, {
|
|
76
|
+
items: () => this._items().map(item => item._btn).filter(Boolean),
|
|
77
|
+
});
|
|
78
|
+
} else if (!on && this._droplet) {
|
|
79
|
+
this._droplet.destroy();
|
|
80
|
+
this._droplet = null;
|
|
81
|
+
}
|
|
82
|
+
this._syncItems();
|
|
83
|
+
}
|
|
84
|
+
|
|
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
|
+
}
|
|
97
|
+
|
|
98
|
+
setupEvents() {
|
|
99
|
+
// The drop's state lives on the nav; the items carry it on their wrappers.
|
|
100
|
+
this._navObserver = new MutationObserver(() => this._syncItems());
|
|
101
|
+
this._navObserver.observe(this._nav, { attributes: true, attributeFilter: ['data-droplet', 'data-droplet-drag'] });
|
|
102
|
+
// Items added or removed.
|
|
103
|
+
this._itemObserver = new MutationObserver(() => this._itemChanged());
|
|
104
|
+
this._itemObserver.observe(this, { childList: true });
|
|
105
|
+
this._applyDroplet();
|
|
50
106
|
}
|
|
51
107
|
|
|
52
108
|
teardownEvents() {
|
|
53
|
-
this.
|
|
109
|
+
this._navObserver?.disconnect();
|
|
110
|
+
this._itemObserver?.disconnect();
|
|
111
|
+
this._droplet?.destroy();
|
|
112
|
+
this._droplet = null;
|
|
54
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); }
|
|
55
122
|
}
|
|
56
123
|
|
|
57
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,10 +78,32 @@ 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);
|
|
86
|
+
this._tellBar();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The bar's look reaches the item through its wrapper. GlassKit's rules for
|
|
91
|
+
* floating and droplet items are descendant selectors of the bar's classes,
|
|
92
|
+
* and this button sits in a shadow root of its own, so <glk-tab-bar> hands
|
|
93
|
+
* its state down and the wrapper carries it. Before 1.27.0 the items of a
|
|
94
|
+
* floating bar kept the fixed bar's look: no spotlight, no pill, the dot.
|
|
95
|
+
*/
|
|
96
|
+
_syncBar({ floating, droplet, placed, drag }) {
|
|
97
|
+
if (!this._wrapper) return;
|
|
98
|
+
this._wrapper.classList.toggle('glass-tab-bar--floating', floating);
|
|
99
|
+
this._wrapper.classList.toggle('glass-tab-bar--droplet', droplet);
|
|
100
|
+
this._wrapper.toggleAttribute('data-droplet', placed);
|
|
101
|
+
this._wrapper.toggleAttribute('data-droplet-drag', drag);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Tells the bar that this item rendered or changed its active state. */
|
|
105
|
+
_tellBar() {
|
|
106
|
+
this.closest('glk-tab-bar')?._itemChanged?.();
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
teardownEvents() {
|
|
@@ -77,6 +115,7 @@ class GlkTabItem extends GlkElement {
|
|
|
77
115
|
switch (name) {
|
|
78
116
|
case 'active':
|
|
79
117
|
this._btn.classList.toggle('is-active', this.getBoolAttr('active'));
|
|
118
|
+
this._tellBar();
|
|
80
119
|
break;
|
|
81
120
|
case 'label':
|
|
82
121
|
this._labelEl.textContent = this.getAttribute('label') || '';
|