@jungherz-de/glasskit-elements 1.19.2 → 1.21.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 +45 -0
- package/README.md +56 -31
- package/SKILL.md +124 -27
- package/dist/components/base.js +185 -12
- package/dist/components/glk-accordion-item.js +20 -5
- package/dist/components/glk-button.js +69 -2
- package/dist/components/glk-calendar.js +3 -0
- package/dist/components/glk-checkbox.js +12 -2
- package/dist/components/glk-input.js +25 -4
- package/dist/components/glk-modal.js +99 -21
- package/dist/components/glk-pill.js +14 -1
- package/dist/components/glk-popover.js +21 -3
- package/dist/components/glk-radio.js +26 -2
- package/dist/components/glk-range.js +3 -1
- package/dist/components/glk-search.js +14 -1
- package/dist/components/glk-segmented.js +4 -0
- package/dist/components/glk-select.js +8 -2
- package/dist/components/glk-textarea.js +8 -2
- package/dist/components/glk-toggle.js +18 -11
- package/dist/glasskit-elements.esm.js +528 -68
- package/dist/glasskit-elements.js +528 -68
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +184 -11
- package/src/components/buttons/glk-button.js +69 -2
- package/src/components/containers/glk-accordion-item.js +20 -5
- package/src/components/feedback/glk-modal.js +99 -21
- package/src/components/feedback/glk-popover.js +21 -3
- package/src/components/forms/glk-calendar.js +3 -0
- package/src/components/forms/glk-checkbox.js +12 -2
- package/src/components/forms/glk-input.js +25 -4
- package/src/components/forms/glk-radio.js +26 -2
- package/src/components/forms/glk-range.js +3 -1
- package/src/components/forms/glk-search.js +14 -1
- package/src/components/forms/glk-segmented.js +4 -0
- package/src/components/forms/glk-select.js +8 -2
- package/src/components/forms/glk-textarea.js +8 -2
- package/src/components/forms/glk-toggle.js +18 -11
- package/src/components/navigation/glk-pill.js +14 -1
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
|
+
// Since 1.20.0 the overlay is a native <dialog>, opened with showModal(). The
|
|
5
|
+
// browser then does what a modal needs: the dialog lies in the top layer, the
|
|
6
|
+
// page behind it is inert — for the pointer, the keyboard and screen readers
|
|
7
|
+
// alike — focus moves in on opening and goes back to where it came from on
|
|
8
|
+
// closing, and the content is out of reach while the modal is closed.
|
|
9
|
+
// GlassKit (1.20.0) styles dialog.glass-modal-overlay for this.
|
|
10
|
+
//
|
|
11
|
+
// The element keeps the fade: the dialog opens before .is-active is set and
|
|
12
|
+
// closes only after the overlay has faded (safety net 400 ms; at once under
|
|
13
|
+
// prefers-reduced-motion). Escape arrives as the dialog's cancel event and
|
|
14
|
+
// takes the same path as a click on the dimmed area.
|
|
15
|
+
//
|
|
16
|
+
// Which element gets the focus on opening, the browsers decide differently:
|
|
17
|
+
// Chrome takes the first field of the content, Safari the first action. An
|
|
18
|
+
// element marked autofocus — in the content or among the actions — settles
|
|
19
|
+
// it in both.
|
|
20
|
+
|
|
21
|
+
const CLOSE_FALLBACK_MS = 400;
|
|
22
|
+
|
|
23
|
+
function reducedMotion() {
|
|
24
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
class GlkModal extends GlkElement {
|
|
5
28
|
static get observedAttributes() {
|
|
6
29
|
return ['open', 'title'];
|
|
@@ -9,13 +32,13 @@ class GlkModal extends GlkElement {
|
|
|
9
32
|
static get observesLightDom() { return true; }
|
|
10
33
|
|
|
11
34
|
render() {
|
|
12
|
-
this._overlay = this.createElement('
|
|
35
|
+
this._overlay = this.createElement('dialog', ['glass-modal-overlay'], { part: 'overlay' });
|
|
13
36
|
|
|
14
37
|
const modal = this.createElement('div', ['glass-modal'], { part: 'modal' });
|
|
15
38
|
|
|
16
|
-
// Header
|
|
39
|
+
// Header — the title names the dialog
|
|
17
40
|
const header = this.createElement('div', ['glass-modal__header'], { part: 'header' });
|
|
18
|
-
this._titleEl = this.createElement('h2', ['glass-modal__title']);
|
|
41
|
+
this._titleEl = this.createElement('h2', ['glass-modal__title'], { id: 'title' });
|
|
19
42
|
this.takeTitle();
|
|
20
43
|
this._applyTitle();
|
|
21
44
|
header.appendChild(this._titleEl);
|
|
@@ -36,10 +59,8 @@ class GlkModal extends GlkElement {
|
|
|
36
59
|
|
|
37
60
|
this._overlay.appendChild(modal);
|
|
38
61
|
this._wrapper.appendChild(this._overlay);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this._overlay.classList.add('is-active');
|
|
42
|
-
}
|
|
62
|
+
// `open` is applied in setupEvents, which also runs when a moved element
|
|
63
|
+
// comes back.
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
projectLightDom() {
|
|
@@ -64,33 +85,49 @@ class GlkModal extends GlkElement {
|
|
|
64
85
|
setupEvents() {
|
|
65
86
|
// Close on overlay click (outside modal)
|
|
66
87
|
this._onOverlayClick = (e) => {
|
|
67
|
-
if (e.target === this._overlay)
|
|
68
|
-
this.removeAttribute('open');
|
|
69
|
-
this.emit('glk-close');
|
|
70
|
-
}
|
|
88
|
+
if (e.target === this._overlay) this._dismiss();
|
|
71
89
|
};
|
|
72
90
|
this._overlay.addEventListener('click', this._onOverlayClick);
|
|
73
91
|
|
|
74
|
-
//
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.emit('glk-close');
|
|
79
|
-
}
|
|
92
|
+
// Escape: the dialog would close at once; it fades out like a click outside.
|
|
93
|
+
this._onCancel = (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
this._dismiss();
|
|
80
96
|
};
|
|
81
|
-
|
|
97
|
+
this._overlay.addEventListener('cancel', this._onCancel);
|
|
98
|
+
|
|
99
|
+
// The browser may also close the dialog by itself — Chrome does on a
|
|
100
|
+
// second Escape without user activation in between, skipping cancel.
|
|
101
|
+
this._onClose = () => {
|
|
102
|
+
if (this._overlay.open) return; // a late event from an earlier close
|
|
103
|
+
this._overlay.classList.remove('is-active');
|
|
104
|
+
if (this.open) this._dismiss();
|
|
105
|
+
};
|
|
106
|
+
this._overlay.addEventListener('close', this._onClose);
|
|
107
|
+
|
|
108
|
+
if (this.open) this._show();
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
teardownEvents() {
|
|
85
112
|
this._overlay?.removeEventListener('click', this._onOverlayClick);
|
|
86
|
-
|
|
113
|
+
this._overlay?.removeEventListener('cancel', this._onCancel);
|
|
114
|
+
this._overlay?.removeEventListener('close', this._onClose);
|
|
115
|
+
// Taken out of the document, the dialog leaves the top layer but stays
|
|
116
|
+
// open — shown in place, not modal. Closed now, it opens as a modal again
|
|
117
|
+
// when the element comes back.
|
|
118
|
+
clearTimeout(this._closeTimer);
|
|
119
|
+
this._overlay?.removeEventListener('transitionend', this._onFaded);
|
|
120
|
+
this._overlay?.classList.remove('is-active');
|
|
121
|
+
if (this._overlay?.open) this._overlay.close();
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
onAttributeChanged(name, _old, value) {
|
|
90
125
|
if (!this._overlay) return;
|
|
91
126
|
switch (name) {
|
|
92
127
|
case 'open':
|
|
93
|
-
|
|
128
|
+
if (!this.isConnected) return; // setupEvents applies it on connect
|
|
129
|
+
if (this.open) this._show();
|
|
130
|
+
else this._hide();
|
|
94
131
|
break;
|
|
95
132
|
case 'title':
|
|
96
133
|
if (this.takeTitle(value)) this._applyTitle();
|
|
@@ -98,11 +135,52 @@ class GlkModal extends GlkElement {
|
|
|
98
135
|
}
|
|
99
136
|
}
|
|
100
137
|
|
|
138
|
+
_show() {
|
|
139
|
+
const dialog = this._overlay;
|
|
140
|
+
clearTimeout(this._closeTimer);
|
|
141
|
+
dialog.removeEventListener('transitionend', this._onFaded);
|
|
142
|
+
if (!dialog.open) {
|
|
143
|
+
dialog.showModal();
|
|
144
|
+
// The actions are copies in the shadow root; the originals are not shown.
|
|
145
|
+
(this._footer.querySelector('[autofocus]') || this.querySelector('[autofocus]'))?.focus();
|
|
146
|
+
}
|
|
147
|
+
void dialog.offsetHeight; // reflow, so the fade starts from the closed state
|
|
148
|
+
requestAnimationFrame(() => {
|
|
149
|
+
if (this.open) dialog.classList.add('is-active');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_hide() {
|
|
154
|
+
const dialog = this._overlay;
|
|
155
|
+
dialog.classList.remove('is-active');
|
|
156
|
+
this._onFaded = (event) => {
|
|
157
|
+
if (event && event.target !== dialog) return; // the panel's own transition
|
|
158
|
+
clearTimeout(this._closeTimer);
|
|
159
|
+
dialog.removeEventListener('transitionend', this._onFaded);
|
|
160
|
+
if (!this.open && dialog.open) dialog.close();
|
|
161
|
+
};
|
|
162
|
+
if (!dialog.open || reducedMotion()) {
|
|
163
|
+
this._onFaded();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
dialog.addEventListener('transitionend', this._onFaded);
|
|
167
|
+
this._closeTimer = setTimeout(this._onFaded, CLOSE_FALLBACK_MS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Closed by the user — a click outside, Escape. */
|
|
171
|
+
_dismiss() {
|
|
172
|
+
this.removeAttribute('open');
|
|
173
|
+
this.emit('glk-close');
|
|
174
|
+
}
|
|
175
|
+
|
|
101
176
|
show() { this.setAttribute('open', ''); }
|
|
102
177
|
close() { this.removeAttribute('open'); }
|
|
103
178
|
|
|
104
179
|
_applyTitle() {
|
|
105
|
-
if (this._titleEl)
|
|
180
|
+
if (!this._titleEl) return;
|
|
181
|
+
this._titleEl.textContent = this._title || '';
|
|
182
|
+
if (this._title) this._overlay.setAttribute('aria-labelledby', 'title');
|
|
183
|
+
else this._overlay.removeAttribute('aria-labelledby');
|
|
106
184
|
}
|
|
107
185
|
|
|
108
186
|
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
|
+
// Handed down to the native button, which is what assistive technology reads —
|
|
5
|
+
// <glk-popover> sets aria-expanded on its trigger.
|
|
6
|
+
const FORWARDED_ARIA = ['aria-expanded', 'aria-haspopup', 'aria-pressed'];
|
|
7
|
+
|
|
4
8
|
class GlkPill extends GlkElement {
|
|
5
9
|
static get observedAttributes() {
|
|
6
|
-
return ['label', 'disabled'];
|
|
10
|
+
return ['label', 'disabled', ...FORWARDED_ARIA];
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
render() {
|
|
@@ -13,6 +17,7 @@ class GlkPill extends GlkElement {
|
|
|
13
17
|
|
|
14
18
|
const ariaLabel = this.getAttribute('label');
|
|
15
19
|
if (ariaLabel) this._btn.setAttribute('aria-label', ariaLabel);
|
|
20
|
+
for (const attr of FORWARDED_ARIA) this._forward(attr);
|
|
16
21
|
|
|
17
22
|
// Slotted content (SVG icons, text)
|
|
18
23
|
this._btn.appendChild(document.createElement('slot'));
|
|
@@ -41,8 +46,16 @@ class GlkPill extends GlkElement {
|
|
|
41
46
|
case 'disabled':
|
|
42
47
|
this._btn.disabled = this.getBoolAttr('disabled');
|
|
43
48
|
break;
|
|
49
|
+
default:
|
|
50
|
+
if (FORWARDED_ARIA.includes(name)) this._forward(name);
|
|
44
51
|
}
|
|
45
52
|
}
|
|
53
|
+
|
|
54
|
+
_forward(attr) {
|
|
55
|
+
const value = this.getAttribute(attr);
|
|
56
|
+
if (value === null) this._btn.removeAttribute(attr);
|
|
57
|
+
else this._btn.setAttribute(attr, value);
|
|
58
|
+
}
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
customElements.define('glk-pill', GlkPill);
|
|
@@ -11,9 +11,9 @@ class GlkPopover extends GlkElement {
|
|
|
11
11
|
render() {
|
|
12
12
|
this._anchor = this.createElement('div', ['glass-popover-anchor']);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this._anchor.appendChild(
|
|
14
|
+
this._triggerSlot = document.createElement('slot');
|
|
15
|
+
this._triggerSlot.setAttribute('name', 'trigger');
|
|
16
|
+
this._anchor.appendChild(this._triggerSlot);
|
|
17
17
|
|
|
18
18
|
this._popover = this.createElement('div', ['glass-popover']);
|
|
19
19
|
this._applyPlacement();
|
|
@@ -37,7 +37,23 @@ class GlkPopover extends GlkElement {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The trigger says whether the popover is open. It lives in the light DOM,
|
|
42
|
+
* so aria-expanded goes on it directly; <glk-button> and <glk-pill> hand
|
|
43
|
+
* it on to their native button. Runs again when the trigger is swapped.
|
|
44
|
+
*/
|
|
45
|
+
_applyExpanded() {
|
|
46
|
+
const open = String(this.getBoolAttr('open'));
|
|
47
|
+
for (const trigger of this._triggerSlot.assignedElements()) {
|
|
48
|
+
trigger.setAttribute('aria-expanded', open);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
setupEvents() {
|
|
53
|
+
this._onSlotChange = () => this._applyExpanded();
|
|
54
|
+
this._triggerSlot.addEventListener('slotchange', this._onSlotChange);
|
|
55
|
+
this._applyExpanded();
|
|
56
|
+
|
|
41
57
|
// Toggle when the slotted trigger is clicked.
|
|
42
58
|
this._onTriggerClick = (e) => {
|
|
43
59
|
const path = e.composedPath();
|
|
@@ -69,6 +85,7 @@ class GlkPopover extends GlkElement {
|
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
teardownEvents() {
|
|
88
|
+
this._triggerSlot?.removeEventListener('slotchange', this._onSlotChange);
|
|
72
89
|
this.removeEventListener('click', this._onTriggerClick);
|
|
73
90
|
document.removeEventListener('click', this._onDocClick);
|
|
74
91
|
document.removeEventListener('keydown', this._onKeydown);
|
|
@@ -80,6 +97,7 @@ class GlkPopover extends GlkElement {
|
|
|
80
97
|
case 'open': {
|
|
81
98
|
const isOpen = this.getBoolAttr('open');
|
|
82
99
|
this._popover.classList.toggle('is-open', isOpen);
|
|
100
|
+
this._applyExpanded();
|
|
83
101
|
this.emit(isOpen ? 'glk-open' : 'glk-close');
|
|
84
102
|
break;
|
|
85
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GlkFormElement } from './base.js';
|
|
1
|
+
import { GlkFormElement, checkControlSheet, forwardHostClicks } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
4
|
// ── Grouping ──
|
|
@@ -30,9 +30,11 @@ function syncGroupTabIndex(group) {
|
|
|
30
30
|
|
|
31
31
|
class GlkRadio extends GlkFormElement {
|
|
32
32
|
static get observedAttributes() {
|
|
33
|
-
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
33
|
+
return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
static get hostStyles() { return checkControlSheet; }
|
|
37
|
+
|
|
36
38
|
render() {
|
|
37
39
|
const label = this.createElement('label', ['glass-radio']);
|
|
38
40
|
|
|
@@ -87,11 +89,13 @@ class GlkRadio extends GlkFormElement {
|
|
|
87
89
|
|
|
88
90
|
this._input.addEventListener('change', this._onChange);
|
|
89
91
|
this._input.addEventListener('keydown', this._onKeyDown);
|
|
92
|
+
this._unforwardClicks = forwardHostClicks(this, () => this._input);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
teardownEvents() {
|
|
93
96
|
this._input?.removeEventListener('change', this._onChange);
|
|
94
97
|
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
98
|
+
this._unforwardClicks?.();
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
@@ -126,6 +130,26 @@ class GlkRadio extends GlkFormElement {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
get _validityField() { return this._input; }
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Radios are missing a value as a group: one required radio makes the
|
|
137
|
+
* whole group required, and any checked one satisfies it. Each input here
|
|
138
|
+
* sits alone in its shadow root, so the group's verdict is worked out across
|
|
139
|
+
* the elements and handed to every member. The inner input carries
|
|
140
|
+
* `required` only while the group lacks a value — that way it produces the
|
|
141
|
+
* browser's own message for it.
|
|
142
|
+
*/
|
|
143
|
+
syncValidity() {
|
|
144
|
+
const group = this._group();
|
|
145
|
+
const missing = group.some(el => el.hasAttribute('required')) && !group.some(el => el.checked);
|
|
146
|
+
for (const el of group) {
|
|
147
|
+
if (!el._input) continue; // not rendered yet; syncs when it is
|
|
148
|
+
el._input.required = missing;
|
|
149
|
+
super.syncValidity.call(el);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
onAttributeChanged(name) {
|
|
130
154
|
if (this._syncing) return;
|
|
131
155
|
if (!this._input) return;
|
|
@@ -11,7 +11,8 @@ class GlkRange extends GlkFormElement {
|
|
|
11
11
|
|
|
12
12
|
const header = this.createElement('div', ['glass-range-header']);
|
|
13
13
|
|
|
14
|
-
this
|
|
14
|
+
// Tied to the slider by an id that only has to be unique in this shadow root.
|
|
15
|
+
this._labelEl = this.createElement('label', [], { for: 'field' });
|
|
15
16
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
16
17
|
|
|
17
18
|
this._valueEl = this.createElement('span', ['glass-range-value']);
|
|
@@ -20,6 +21,7 @@ class GlkRange extends GlkFormElement {
|
|
|
20
21
|
header.appendChild(this._valueEl);
|
|
21
22
|
|
|
22
23
|
this._input = this.createElement('input', ['glass-range'], {
|
|
24
|
+
id: 'field',
|
|
23
25
|
type: 'range',
|
|
24
26
|
min: this.getAttribute('min') || '0',
|
|
25
27
|
max: this.getAttribute('max') || '100',
|
|
@@ -5,7 +5,7 @@ const SEARCH_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
|
|
5
5
|
|
|
6
6
|
class GlkSearch extends GlkFormElement {
|
|
7
7
|
static get observedAttributes() {
|
|
8
|
-
return ['placeholder', 'name', 'value', 'disabled'];
|
|
8
|
+
return ['placeholder', 'name', 'value', 'disabled', 'label'];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
render() {
|
|
@@ -21,6 +21,10 @@ class GlkSearch extends GlkFormElement {
|
|
|
21
21
|
const placeholder = this.getAttribute('placeholder');
|
|
22
22
|
if (placeholder) this._input.setAttribute('placeholder', placeholder);
|
|
23
23
|
|
|
24
|
+
// The search field has no visible label; `label` names it for assistive
|
|
25
|
+
// technology. Without it the placeholder is all a screen reader can say.
|
|
26
|
+
this._applyLabel();
|
|
27
|
+
|
|
24
28
|
const name = this.getAttribute('name');
|
|
25
29
|
if (name) this._input.setAttribute('name', name);
|
|
26
30
|
|
|
@@ -65,9 +69,18 @@ class GlkSearch extends GlkFormElement {
|
|
|
65
69
|
case 'disabled':
|
|
66
70
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
67
71
|
break;
|
|
72
|
+
case 'label':
|
|
73
|
+
this._applyLabel();
|
|
74
|
+
break;
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
_applyLabel() {
|
|
79
|
+
const label = this.getAttribute('label');
|
|
80
|
+
if (label) this._input.setAttribute('aria-label', label);
|
|
81
|
+
else this._input.removeAttribute('aria-label');
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
_syncFormValue() {
|
|
72
85
|
this.setFormValue(this._input.value);
|
|
73
86
|
}
|
|
@@ -40,6 +40,10 @@ function parseList(json) {
|
|
|
40
40
|
class GlkSegmented extends GlkFormElement {
|
|
41
41
|
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
|
|
42
42
|
|
|
43
|
+
// A row of buttons with one tab stop: delegated focus would land on the
|
|
44
|
+
// first button, not the chosen one.
|
|
45
|
+
static get delegatesFocus() { return false; }
|
|
46
|
+
|
|
43
47
|
render() {
|
|
44
48
|
this._group = this.createElement('div', ['glass-segmented'], { role: 'group', part: 'group' });
|
|
45
49
|
this._wrapper.appendChild(this._group);
|
|
@@ -11,10 +11,11 @@ class GlkSelect extends GlkFormElement {
|
|
|
11
11
|
render() {
|
|
12
12
|
const group = this.createElement('div', ['glass-input-group']);
|
|
13
13
|
|
|
14
|
-
this
|
|
14
|
+
// Tied to the field by an id that only has to be unique in this shadow root.
|
|
15
|
+
this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
|
|
15
16
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
16
17
|
|
|
17
|
-
this._select = this.createElement('select', ['glass-select']);
|
|
18
|
+
this._select = this.createElement('select', ['glass-select'], { id: 'field' });
|
|
18
19
|
|
|
19
20
|
const name = this.getAttribute('name');
|
|
20
21
|
if (name) this._select.setAttribute('name', name);
|
|
@@ -102,9 +103,14 @@ class GlkSelect extends GlkFormElement {
|
|
|
102
103
|
this._applyValue(this.getAttribute('value'));
|
|
103
104
|
this._syncFormValue();
|
|
104
105
|
break;
|
|
106
|
+
case 'required':
|
|
107
|
+
this._select.required = this.getBoolAttr('required');
|
|
108
|
+
break;
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
get _validityField() { return this._select; }
|
|
113
|
+
|
|
108
114
|
_syncFormValue() {
|
|
109
115
|
this.setFormValue(this._select.value);
|
|
110
116
|
}
|
|
@@ -9,10 +9,11 @@ class GlkTextarea extends GlkFormElement {
|
|
|
9
9
|
render() {
|
|
10
10
|
const group = this.createElement('div', ['glass-input-group']);
|
|
11
11
|
|
|
12
|
-
this
|
|
12
|
+
// Tied to the field by an id that only has to be unique in this shadow root.
|
|
13
|
+
this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
|
|
13
14
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
14
15
|
|
|
15
|
-
this._textarea = this.createElement('textarea', ['glass-textarea']);
|
|
16
|
+
this._textarea = this.createElement('textarea', ['glass-textarea'], { id: 'field' });
|
|
16
17
|
const placeholder = this.getAttribute('placeholder');
|
|
17
18
|
if (placeholder) this._textarea.setAttribute('placeholder', placeholder);
|
|
18
19
|
|
|
@@ -70,9 +71,14 @@ class GlkTextarea extends GlkFormElement {
|
|
|
70
71
|
this._textarea.value = this.getAttribute('value') || '';
|
|
71
72
|
this._syncFormValue();
|
|
72
73
|
break;
|
|
74
|
+
case 'required':
|
|
75
|
+
this._textarea.required = this.getBoolAttr('required');
|
|
76
|
+
break;
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
get _validityField() { return this._textarea; }
|
|
81
|
+
|
|
76
82
|
_syncFormValue() {
|
|
77
83
|
this.setFormValue(this._textarea.value);
|
|
78
84
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { GlkFormElement } from './base.js';
|
|
1
|
+
import { GlkFormElement, checkControlSheet, forwardHostClicks } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
4
|
class GlkToggle extends GlkFormElement {
|
|
5
5
|
static get observedAttributes() {
|
|
6
|
-
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
6
|
+
return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
static get hostStyles() { return checkControlSheet; }
|
|
10
|
+
|
|
9
11
|
render() {
|
|
10
12
|
const label = this.createElement('label', ['glass-toggle']);
|
|
11
13
|
|
|
14
|
+
// The native checkbox is the switch: role, checked state and name (from
|
|
15
|
+
// the label around it) all sit on one control. The host has no role of
|
|
16
|
+
// its own — it used to carry role="switch" as well, and screen readers met
|
|
17
|
+
// a switch with a checkbox inside.
|
|
12
18
|
this._input = this.createElement('input', ['glass-toggle__input'], {
|
|
13
|
-
type: 'checkbox'
|
|
19
|
+
type: 'checkbox',
|
|
20
|
+
role: 'switch'
|
|
14
21
|
});
|
|
15
22
|
|
|
16
23
|
const name = this.getAttribute('name');
|
|
@@ -29,14 +36,12 @@ class GlkToggle extends GlkFormElement {
|
|
|
29
36
|
|
|
30
37
|
if (this.getBoolAttr('checked')) this._input.checked = true;
|
|
31
38
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
39
|
+
if (this.getBoolAttr('required')) this._input.required = true;
|
|
32
40
|
|
|
33
41
|
this._defaultChecked = this.getBoolAttr('checked');
|
|
34
42
|
this._wrapper.appendChild(label);
|
|
35
43
|
|
|
36
44
|
this._syncFormValue();
|
|
37
|
-
this.setAttribute('role', 'switch');
|
|
38
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
39
|
-
if (this.getBoolAttr('disabled')) this.setAttribute('aria-disabled', 'true');
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
setupEvents() {
|
|
@@ -44,16 +49,17 @@ class GlkToggle extends GlkFormElement {
|
|
|
44
49
|
this._syncing = true;
|
|
45
50
|
this.setBoolAttr('checked', this._input.checked);
|
|
46
51
|
this._syncing = false;
|
|
47
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
48
52
|
this._syncFormValue();
|
|
49
53
|
this.emit('glk-change', { checked: this._input.checked });
|
|
50
54
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
51
55
|
};
|
|
52
56
|
this._input.addEventListener('change', this._onChange);
|
|
57
|
+
this._unforwardClicks = forwardHostClicks(this, () => this._input);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
teardownEvents() {
|
|
56
61
|
this._input?.removeEventListener('change', this._onChange);
|
|
62
|
+
this._unforwardClicks?.();
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
onAttributeChanged(name) {
|
|
@@ -62,12 +68,10 @@ class GlkToggle extends GlkFormElement {
|
|
|
62
68
|
switch (name) {
|
|
63
69
|
case 'checked':
|
|
64
70
|
this._input.checked = this.getBoolAttr('checked');
|
|
65
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
66
71
|
this._syncFormValue();
|
|
67
72
|
break;
|
|
68
73
|
case 'disabled':
|
|
69
74
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
70
|
-
this.setAttribute('aria-disabled', String(this.getBoolAttr('disabled')));
|
|
71
75
|
break;
|
|
72
76
|
case 'label':
|
|
73
77
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
@@ -75,9 +79,14 @@ class GlkToggle extends GlkFormElement {
|
|
|
75
79
|
case 'name':
|
|
76
80
|
this._input.setAttribute('name', this.getAttribute('name') || '');
|
|
77
81
|
break;
|
|
82
|
+
case 'required':
|
|
83
|
+
this._input.required = this.getBoolAttr('required');
|
|
84
|
+
break;
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
get _validityField() { return this._input; }
|
|
89
|
+
|
|
81
90
|
_syncFormValue() {
|
|
82
91
|
const val = this.getAttribute('value') || 'on';
|
|
83
92
|
this.setFormValue(this._input.checked ? val : null);
|
|
@@ -86,7 +95,6 @@ class GlkToggle extends GlkFormElement {
|
|
|
86
95
|
resetValue() {
|
|
87
96
|
this._input.checked = this._defaultChecked;
|
|
88
97
|
this.setBoolAttr('checked', this._defaultChecked);
|
|
89
|
-
this.setAttribute('aria-checked', String(this._defaultChecked));
|
|
90
98
|
this._syncFormValue();
|
|
91
99
|
}
|
|
92
100
|
|
|
@@ -101,7 +109,6 @@ class GlkToggle extends GlkFormElement {
|
|
|
101
109
|
set checked(v) {
|
|
102
110
|
if (this._input) this._input.checked = v;
|
|
103
111
|
this.setBoolAttr('checked', v);
|
|
104
|
-
this.setAttribute('aria-checked', String(v));
|
|
105
112
|
this._syncFormValue();
|
|
106
113
|
}
|
|
107
114
|
|