@jungherz-de/glasskit-elements 1.10.0 → 1.12.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 +3 -3
- package/dist/components/glk-accordion-item.js +1 -1
- package/dist/components/glk-accordion.js +1 -1
- package/dist/components/glk-avatar.js +1 -1
- package/dist/components/glk-badge.js +1 -1
- package/dist/components/glk-button.js +1 -1
- package/dist/components/glk-card.js +1 -1
- package/dist/components/glk-checkbox.js +1 -1
- package/dist/components/glk-divider.js +1 -1
- package/dist/components/glk-input.js +1 -1
- package/dist/components/glk-list-item.js +1 -1
- package/dist/components/glk-list.js +1 -1
- package/dist/components/glk-modal.js +19 -12
- package/dist/components/glk-nav.js +1 -1
- package/dist/components/glk-pill.js +1 -1
- package/dist/components/glk-popover.js +1 -1
- package/dist/components/glk-progress.js +1 -1
- package/dist/components/glk-radio.js +89 -8
- package/dist/components/glk-range.js +1 -1
- package/dist/components/glk-search.js +1 -1
- package/dist/components/glk-select.js +39 -8
- package/dist/components/glk-status.js +1 -1
- package/dist/components/glk-tab-accessory.js +1 -1
- package/dist/components/glk-tab-bar.js +1 -1
- package/dist/components/glk-tab-dock.js +1 -1
- package/dist/components/glk-tab-item.js +26 -15
- package/dist/components/glk-textarea.js +1 -1
- package/dist/components/glk-title.js +1 -1
- package/dist/components/glk-toast.js +1 -1
- package/dist/components/glk-toggle.js +1 -1
- package/dist/components/shared/base-BoN33KPe.js +273 -0
- package/dist/glasskit-elements.esm.js +208 -45
- package/dist/glasskit-elements.js +208 -45
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +36 -3
- package/src/components/feedback/glk-modal.js +18 -11
- package/src/components/forms/glk-radio.js +88 -7
- package/src/components/forms/glk-select.js +38 -7
- package/src/components/navigation/glk-tab-item.js +25 -14
- package/dist/components/shared/base-BzDV6jE3.js +0 -240
|
@@ -5,6 +5,8 @@ class GlkModal extends GlkElement {
|
|
|
5
5
|
return ['open', 'title'];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
static get observesLightDom() { return true; }
|
|
9
|
+
|
|
8
10
|
render() {
|
|
9
11
|
this._overlay = this.createElement('div', ['glass-modal-overlay'], { part: 'overlay' });
|
|
10
12
|
|
|
@@ -28,7 +30,7 @@ class GlkModal extends GlkElement {
|
|
|
28
30
|
modal.appendChild(this._footer);
|
|
29
31
|
|
|
30
32
|
// Defer footer population — children may not be parsed yet
|
|
31
|
-
requestAnimationFrame(() => this.
|
|
33
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
32
34
|
|
|
33
35
|
this._overlay.appendChild(modal);
|
|
34
36
|
this._wrapper.appendChild(this._overlay);
|
|
@@ -38,18 +40,23 @@ class GlkModal extends GlkElement {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
projectLightDom() {
|
|
44
|
+
const buttons = [...this.querySelectorAll('[slot="actions"] button')];
|
|
45
|
+
const signature = buttons.map(b => b.outerHTML).join('');
|
|
46
|
+
if (signature === this._footerSignature) return;
|
|
47
|
+
this._footerSignature = signature;
|
|
48
|
+
|
|
42
49
|
this._footer.innerHTML = '';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this._footer.appendChild(clone);
|
|
50
|
+
buttons.forEach((btn, i) => {
|
|
51
|
+
const clone = btn.cloneNode(true);
|
|
52
|
+
// Look the original up again at click time instead of closing over it: a
|
|
53
|
+
// framework that re-renders replaces the node, and a captured reference
|
|
54
|
+
// would then forward the click to a button no longer in the document.
|
|
55
|
+
clone.addEventListener('click', () => {
|
|
56
|
+
this.querySelectorAll('[slot="actions"] button')[i]?.click();
|
|
51
57
|
});
|
|
52
|
-
|
|
58
|
+
this._footer.appendChild(clone);
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
setupEvents() {
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { GlkFormElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
+
// ── Grouping ──
|
|
4
|
+
// Every <glk-radio> keeps its <input type="radio"> in its own shadow root, and
|
|
5
|
+
// native radio grouping works per tree — it does not reach across shadow
|
|
6
|
+
// boundaries. Two <glk-radio name="x"> would therefore both stay checked. So
|
|
7
|
+
// the group is kept here instead, following the native definition as closely
|
|
8
|
+
// as we can: same `name`, same containing tree, same form owner.
|
|
9
|
+
|
|
10
|
+
const ARROW_KEYS = {
|
|
11
|
+
ArrowDown: 1, ArrowRight: 1,
|
|
12
|
+
ArrowUp: -1, ArrowLeft: -1
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Form owner, also for a peer that has not been upgraded yet. */
|
|
16
|
+
function ownerForm(el) {
|
|
17
|
+
return (el.form !== undefined ? el.form : el.closest('form')) ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Only the selected radio is a tab stop; arrow keys move within the group. */
|
|
21
|
+
function syncGroupTabIndex(group) {
|
|
22
|
+
const enabled = group.filter(el => !el.disabled);
|
|
23
|
+
if (!enabled.length) return;
|
|
24
|
+
const focusable = enabled.find(el => el.checked) || enabled[0];
|
|
25
|
+
for (const el of group) {
|
|
26
|
+
if (el._input) el._input.tabIndex = el === focusable ? 0 : -1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
class GlkRadio extends GlkFormElement {
|
|
4
31
|
static get observedAttributes() {
|
|
5
32
|
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
@@ -35,22 +62,67 @@ class GlkRadio extends GlkFormElement {
|
|
|
35
62
|
this._defaultChecked = this.getBoolAttr('checked');
|
|
36
63
|
this._wrapper.appendChild(label);
|
|
37
64
|
this._syncFormValue();
|
|
65
|
+
|
|
66
|
+
// Elements upgrade in document order, so the last `checked` one in the
|
|
67
|
+
// markup wins the group — the same outcome native radios produce.
|
|
68
|
+
if (this._input.checked) this._uncheckPeers();
|
|
69
|
+
syncGroupTabIndex(this._group());
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
setupEvents() {
|
|
41
|
-
this._onChange = () =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
this._onChange = () => this._applyChange();
|
|
74
|
+
|
|
75
|
+
this._onKeyDown = (e) => {
|
|
76
|
+
const dir = ARROW_KEYS[e.key];
|
|
77
|
+
if (!dir || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
78
|
+
const group = this._group().filter(el => !el.disabled);
|
|
79
|
+
if (group.length < 2) return;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const next = group[(group.indexOf(this) + dir + group.length) % group.length];
|
|
82
|
+
next._input.focus();
|
|
83
|
+
next._input.checked = true;
|
|
84
|
+
next._applyChange();
|
|
48
85
|
};
|
|
86
|
+
|
|
49
87
|
this._input.addEventListener('change', this._onChange);
|
|
88
|
+
this._input.addEventListener('keydown', this._onKeyDown);
|
|
50
89
|
}
|
|
51
90
|
|
|
52
91
|
teardownEvents() {
|
|
53
92
|
this._input?.removeEventListener('change', this._onChange);
|
|
93
|
+
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
97
|
+
_applyChange() {
|
|
98
|
+
if (this._input.checked) this._uncheckPeers();
|
|
99
|
+
this._syncing = true;
|
|
100
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
101
|
+
this._syncing = false;
|
|
102
|
+
this._syncFormValue();
|
|
103
|
+
syncGroupTabIndex(this._group());
|
|
104
|
+
this.emit('glk-change', { checked: this._input.checked, value: this._input.value });
|
|
105
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The radio group per the HTML definition: same name, same tree, same form
|
|
110
|
+
* owner — in document order, this element included.
|
|
111
|
+
*/
|
|
112
|
+
_group() {
|
|
113
|
+
const name = this.getAttribute('name');
|
|
114
|
+
const root = this.getRootNode();
|
|
115
|
+
if (!name || typeof root?.querySelectorAll !== 'function') return [this];
|
|
116
|
+
const form = ownerForm(this);
|
|
117
|
+
return [...root.querySelectorAll('glk-radio')].filter(
|
|
118
|
+
el => el.getAttribute('name') === name && ownerForm(el) === form
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_uncheckPeers() {
|
|
123
|
+
for (const el of this._group()) {
|
|
124
|
+
if (el !== this && el.checked) el.checked = false;
|
|
125
|
+
}
|
|
54
126
|
}
|
|
55
127
|
|
|
56
128
|
onAttributeChanged(name) {
|
|
@@ -59,16 +131,20 @@ class GlkRadio extends GlkFormElement {
|
|
|
59
131
|
switch (name) {
|
|
60
132
|
case 'checked':
|
|
61
133
|
this._input.checked = this.getBoolAttr('checked');
|
|
134
|
+
if (this._input.checked) this._uncheckPeers();
|
|
62
135
|
this._syncFormValue();
|
|
136
|
+
syncGroupTabIndex(this._group());
|
|
63
137
|
break;
|
|
64
138
|
case 'disabled':
|
|
65
139
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
140
|
+
syncGroupTabIndex(this._group());
|
|
66
141
|
break;
|
|
67
142
|
case 'label':
|
|
68
143
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
69
144
|
break;
|
|
70
145
|
case 'name':
|
|
71
146
|
this._input.setAttribute('name', this.getAttribute('name') || '');
|
|
147
|
+
syncGroupTabIndex(this._group());
|
|
72
148
|
break;
|
|
73
149
|
case 'value':
|
|
74
150
|
this._input.setAttribute('value', this.getAttribute('value') || '');
|
|
@@ -86,13 +162,18 @@ class GlkRadio extends GlkFormElement {
|
|
|
86
162
|
this._input.checked = this._defaultChecked;
|
|
87
163
|
this.setBoolAttr('checked', this._defaultChecked);
|
|
88
164
|
this._syncFormValue();
|
|
165
|
+
syncGroupTabIndex(this._group());
|
|
89
166
|
}
|
|
90
167
|
|
|
91
168
|
get checked() { return this._input?.checked ?? false; }
|
|
92
169
|
set checked(v) {
|
|
93
170
|
if (this._input) this._input.checked = v;
|
|
171
|
+
// Also covers the case where the attribute is already present, so
|
|
172
|
+
// setBoolAttr stays silent and onAttributeChanged never runs.
|
|
173
|
+
if (v) this._uncheckPeers();
|
|
94
174
|
this.setBoolAttr('checked', v);
|
|
95
175
|
this._syncFormValue();
|
|
176
|
+
syncGroupTabIndex(this._group());
|
|
96
177
|
}
|
|
97
178
|
|
|
98
179
|
get disabled() { return this.getBoolAttr('disabled'); }
|
|
@@ -5,6 +5,8 @@ class GlkSelect extends GlkFormElement {
|
|
|
5
5
|
return ['label', 'disabled', 'name', 'value', 'required'];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
static get observesLightDom() { return true; }
|
|
9
|
+
|
|
8
10
|
render() {
|
|
9
11
|
const group = this.createElement('div', ['glass-input-group']);
|
|
10
12
|
|
|
@@ -25,12 +27,29 @@ class GlkSelect extends GlkFormElement {
|
|
|
25
27
|
this._wrapper.appendChild(group);
|
|
26
28
|
|
|
27
29
|
// Defer option copying — children may not be parsed yet in connectedCallback
|
|
28
|
-
requestAnimationFrame(() =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
projectLightDom() {
|
|
34
|
+
// The options are pure data — the clones carry no listeners — so skipping an
|
|
35
|
+
// unchanged rebuild is safe, and it keeps an open dropdown from snapping shut
|
|
36
|
+
// on light-DOM churn elsewhere.
|
|
37
|
+
const signature = [...this.querySelectorAll('option')].map(o => o.outerHTML).join('');
|
|
38
|
+
if (signature === this._optionSignature) return;
|
|
39
|
+
this._optionSignature = signature;
|
|
40
|
+
|
|
41
|
+
// innerHTML = '' below drops the selection, so remember it first. A
|
|
42
|
+
// selectedIndex of -1 means "nothing is selected", which is not the same as
|
|
43
|
+
// an option whose value happens to be the empty string.
|
|
44
|
+
const previous = this._select.selectedIndex >= 0 ? this._select.value : null;
|
|
45
|
+
|
|
46
|
+
this._moveOptions();
|
|
47
|
+
|
|
48
|
+
// Keep the live selection when it survived the rebuild; otherwise fall back
|
|
49
|
+
// to the value attribute. Without this the selection jumps back to the first
|
|
50
|
+
// entry every time the list is updated.
|
|
51
|
+
if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
|
|
52
|
+
this._syncFormValue();
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
_moveOptions() {
|
|
@@ -41,6 +60,18 @@ class GlkSelect extends GlkFormElement {
|
|
|
41
60
|
});
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Selects `value` if an option carries it, and reports whether it did. The
|
|
65
|
+
* empty string is a value like any other — "" is a real option in plenty of
|
|
66
|
+
* forms ("detect automatically", "enter your own below").
|
|
67
|
+
*/
|
|
68
|
+
_applyValue(value) {
|
|
69
|
+
if (value === null) return false;
|
|
70
|
+
if (![...this._select.options].some(o => o.value === value)) return false;
|
|
71
|
+
this._select.value = value;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
setupEvents() {
|
|
45
76
|
this._onChange = () => {
|
|
46
77
|
this._syncFormValue();
|
|
@@ -67,7 +98,7 @@ class GlkSelect extends GlkFormElement {
|
|
|
67
98
|
this._select.setAttribute('name', this.getAttribute('name') || '');
|
|
68
99
|
break;
|
|
69
100
|
case 'value':
|
|
70
|
-
this.
|
|
101
|
+
this._applyValue(this.getAttribute('value'));
|
|
71
102
|
this._syncFormValue();
|
|
72
103
|
break;
|
|
73
104
|
}
|
|
@@ -5,6 +5,8 @@ class GlkTabItem extends GlkElement {
|
|
|
5
5
|
return ['label', 'active', 'badge'];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
static get observesLightDom() { return true; }
|
|
9
|
+
|
|
8
10
|
render() {
|
|
9
11
|
this._btn = this.createElement('button', ['glass-tab-bar__item']);
|
|
10
12
|
if (this.getBoolAttr('active')) this._btn.classList.add('is-active');
|
|
@@ -30,23 +32,32 @@ class GlkTabItem extends GlkElement {
|
|
|
30
32
|
this._wrapper.appendChild(this._btn);
|
|
31
33
|
|
|
32
34
|
// Defer icon cloning — children may not be parsed yet
|
|
33
|
-
requestAnimationFrame(() => this.
|
|
35
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
projectLightDom() {
|
|
37
39
|
const svg = this.querySelector('svg');
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
const signature = svg ? svg.outerHTML : '';
|
|
41
|
+
if (signature === this._iconSignature) return;
|
|
42
|
+
this._iconSignature = signature;
|
|
43
|
+
|
|
44
|
+
// Drop the previous clone by hand: the icon container also holds the badge,
|
|
45
|
+
// and inserting without removing would stack a second icon on every update.
|
|
46
|
+
this._iconClone?.remove();
|
|
47
|
+
this._iconClone = null;
|
|
48
|
+
if (!svg) return;
|
|
49
|
+
|
|
50
|
+
const clone = svg.cloneNode(true);
|
|
51
|
+
// Remove inline attributes that would override GlassKit styles
|
|
52
|
+
clone.removeAttribute('width');
|
|
53
|
+
clone.removeAttribute('height');
|
|
54
|
+
clone.removeAttribute('stroke');
|
|
55
|
+
clone.removeAttribute('stroke-width');
|
|
56
|
+
clone.removeAttribute('stroke-linecap');
|
|
57
|
+
clone.removeAttribute('stroke-linejoin');
|
|
58
|
+
clone.removeAttribute('fill');
|
|
59
|
+
this._iconEl.insertBefore(clone, this._iconEl.firstChild);
|
|
60
|
+
this._iconClone = clone;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
setupEvents() {
|