@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
|
@@ -10,9 +10,9 @@ class GlkPopover extends GlkElement {
|
|
|
10
10
|
render() {
|
|
11
11
|
this._anchor = this.createElement('div', ['glass-popover-anchor']);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this._anchor.appendChild(
|
|
13
|
+
this._triggerSlot = document.createElement('slot');
|
|
14
|
+
this._triggerSlot.setAttribute('name', 'trigger');
|
|
15
|
+
this._anchor.appendChild(this._triggerSlot);
|
|
16
16
|
|
|
17
17
|
this._popover = this.createElement('div', ['glass-popover']);
|
|
18
18
|
this._applyPlacement();
|
|
@@ -36,7 +36,23 @@ class GlkPopover extends GlkElement {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The trigger says whether the popover is open. It lives in the light DOM,
|
|
41
|
+
* so aria-expanded goes on it directly; <glk-button> and <glk-pill> hand
|
|
42
|
+
* it on to their native button. Runs again when the trigger is swapped.
|
|
43
|
+
*/
|
|
44
|
+
_applyExpanded() {
|
|
45
|
+
const open = String(this.getBoolAttr('open'));
|
|
46
|
+
for (const trigger of this._triggerSlot.assignedElements()) {
|
|
47
|
+
trigger.setAttribute('aria-expanded', open);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
setupEvents() {
|
|
52
|
+
this._onSlotChange = () => this._applyExpanded();
|
|
53
|
+
this._triggerSlot.addEventListener('slotchange', this._onSlotChange);
|
|
54
|
+
this._applyExpanded();
|
|
55
|
+
|
|
40
56
|
// Toggle when the slotted trigger is clicked.
|
|
41
57
|
this._onTriggerClick = (e) => {
|
|
42
58
|
const path = e.composedPath();
|
|
@@ -68,6 +84,7 @@ class GlkPopover extends GlkElement {
|
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
teardownEvents() {
|
|
87
|
+
this._triggerSlot?.removeEventListener('slotchange', this._onSlotChange);
|
|
71
88
|
this.removeEventListener('click', this._onTriggerClick);
|
|
72
89
|
document.removeEventListener('click', this._onDocClick);
|
|
73
90
|
document.removeEventListener('keydown', this._onKeydown);
|
|
@@ -79,6 +96,7 @@ class GlkPopover extends GlkElement {
|
|
|
79
96
|
case 'open': {
|
|
80
97
|
const isOpen = this.getBoolAttr('open');
|
|
81
98
|
this._popover.classList.toggle('is-open', isOpen);
|
|
99
|
+
this._applyExpanded();
|
|
82
100
|
this.emit(isOpen ? 'glk-open' : 'glk-close');
|
|
83
101
|
break;
|
|
84
102
|
}
|
|
@@ -45,6 +45,9 @@ class GlkCalendar extends GlkFormElement {
|
|
|
45
45
|
return ['month', 'value', 'today', 'min', 'max', 'marks', 'locale', 'week-start', 'label', 'prev-label', 'next-label'];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Delegated focus would land on the month arrow, not on the chosen day.
|
|
49
|
+
static get delegatesFocus() { return false; }
|
|
50
|
+
|
|
48
51
|
render() {
|
|
49
52
|
this._root = this.createElement('div', ['glass-calendar'], { part: 'calendar' });
|
|
50
53
|
const head = this.createElement('div', ['glass-calendar__head'], { part: 'head' });
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { GlkFormElement } from '../../base.js';
|
|
1
|
+
import { GlkFormElement, checkControlSheet, forwardHostClicks } from '../../base.js';
|
|
2
2
|
|
|
3
3
|
const CHECKMARK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
4
4
|
|
|
5
5
|
class GlkCheckbox extends GlkFormElement {
|
|
6
6
|
static get observedAttributes() {
|
|
7
|
-
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
7
|
+
return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
static get hostStyles() { return checkControlSheet; }
|
|
11
|
+
|
|
10
12
|
render() {
|
|
11
13
|
const label = this.createElement('label', ['glass-checkbox']);
|
|
12
14
|
|
|
@@ -29,6 +31,7 @@ class GlkCheckbox extends GlkFormElement {
|
|
|
29
31
|
|
|
30
32
|
if (this.getBoolAttr('checked')) this._input.checked = true;
|
|
31
33
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
34
|
+
if (this.getBoolAttr('required')) this._input.required = true;
|
|
32
35
|
|
|
33
36
|
this._defaultChecked = this.getBoolAttr('checked');
|
|
34
37
|
this._wrapper.appendChild(label);
|
|
@@ -45,10 +48,12 @@ class GlkCheckbox extends GlkFormElement {
|
|
|
45
48
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
46
49
|
};
|
|
47
50
|
this._input.addEventListener('change', this._onChange);
|
|
51
|
+
this._unforwardClicks = forwardHostClicks(this, () => this._input);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
teardownEvents() {
|
|
51
55
|
this._input?.removeEventListener('change', this._onChange);
|
|
56
|
+
this._unforwardClicks?.();
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
onAttributeChanged(name) {
|
|
@@ -68,9 +73,14 @@ class GlkCheckbox extends GlkFormElement {
|
|
|
68
73
|
case 'name':
|
|
69
74
|
this._input.setAttribute('name', this.getAttribute('name') || '');
|
|
70
75
|
break;
|
|
76
|
+
case 'required':
|
|
77
|
+
this._input.required = this.getBoolAttr('required');
|
|
78
|
+
break;
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
get _validityField() { return this._input; }
|
|
83
|
+
|
|
74
84
|
_syncFormValue() {
|
|
75
85
|
const val = this.getAttribute('value') || 'on';
|
|
76
86
|
this.setFormValue(this._input.checked ? val : null);
|
|
@@ -14,12 +14,15 @@ class GlkInput extends GlkFormElement {
|
|
|
14
14
|
render() {
|
|
15
15
|
const group = this.createElement('div', ['glass-input-group']);
|
|
16
16
|
|
|
17
|
-
// Label
|
|
18
|
-
this.
|
|
17
|
+
// Label — tied to the field by id, which only has to be unique inside
|
|
18
|
+
// this shadow root. That gives the field its accessible name, and a click
|
|
19
|
+
// on the label text focuses it.
|
|
20
|
+
this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
|
|
19
21
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
20
22
|
|
|
21
23
|
// Input
|
|
22
24
|
this._input = this.createElement('input', this._computeInputClasses(), {
|
|
25
|
+
id: 'field',
|
|
23
26
|
type: this.getAttribute('type') || 'text'
|
|
24
27
|
});
|
|
25
28
|
|
|
@@ -36,8 +39,8 @@ class GlkInput extends GlkFormElement {
|
|
|
36
39
|
if (this.getBoolAttr('required')) this._input.required = true;
|
|
37
40
|
for (const attr of FORWARDED) this._forward(attr);
|
|
38
41
|
|
|
39
|
-
// Hint
|
|
40
|
-
this._hintEl = this.createElement('span', this._computeHintClasses());
|
|
42
|
+
// Hint — read out with the field as its description
|
|
43
|
+
this._hintEl = this.createElement('span', this._computeHintClasses(), { id: 'hint' });
|
|
41
44
|
this._hintEl.textContent = this.getAttribute('hint') || '';
|
|
42
45
|
|
|
43
46
|
group.appendChild(this._labelEl);
|
|
@@ -47,6 +50,7 @@ class GlkInput extends GlkFormElement {
|
|
|
47
50
|
this._group = group;
|
|
48
51
|
this._wrapper.appendChild(group);
|
|
49
52
|
|
|
53
|
+
this._applyDescription();
|
|
50
54
|
this._syncFormValue();
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -84,12 +88,14 @@ class GlkInput extends GlkFormElement {
|
|
|
84
88
|
case 'error':
|
|
85
89
|
this._input.className = this._computeInputClasses().join(' ');
|
|
86
90
|
this._hintEl.className = this._computeHintClasses().join(' ');
|
|
91
|
+
this._applyDescription();
|
|
87
92
|
break;
|
|
88
93
|
case 'hint':
|
|
89
94
|
this._hintEl.textContent = this.getAttribute('hint') || '';
|
|
90
95
|
if (this.getAttribute('hint') && !this._hintEl.parentNode) {
|
|
91
96
|
this._group.appendChild(this._hintEl);
|
|
92
97
|
}
|
|
98
|
+
this._applyDescription();
|
|
93
99
|
break;
|
|
94
100
|
case 'disabled':
|
|
95
101
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
@@ -109,6 +115,19 @@ class GlkInput extends GlkFormElement {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The hint describes the field; with `error` set it is the error message
|
|
120
|
+
* and the field is marked invalid. Server-side errors arrive this way, so
|
|
121
|
+
* `error` stays a marker for assistive technology and does not block the
|
|
122
|
+
* form — only the field's own constraints (required, pattern …) do.
|
|
123
|
+
*/
|
|
124
|
+
_applyDescription() {
|
|
125
|
+
if (this.getAttribute('hint')) this._input.setAttribute('aria-describedby', 'hint');
|
|
126
|
+
else this._input.removeAttribute('aria-describedby');
|
|
127
|
+
if (this.getBoolAttr('error')) this._input.setAttribute('aria-invalid', 'true');
|
|
128
|
+
else this._input.removeAttribute('aria-invalid');
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
_forward(attr) {
|
|
113
132
|
const value = this.getAttribute(attr);
|
|
114
133
|
if (value === null) this._input.removeAttribute(attr);
|
|
@@ -127,6 +146,8 @@ class GlkInput extends GlkFormElement {
|
|
|
127
146
|
return classes;
|
|
128
147
|
}
|
|
129
148
|
|
|
149
|
+
get _validityField() { return this._input; }
|
|
150
|
+
|
|
130
151
|
_syncFormValue() {
|
|
131
152
|
this.setFormValue(this._input.value);
|
|
132
153
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GlkFormElement } from '../../base.js';
|
|
1
|
+
import { GlkFormElement, checkControlSheet, forwardHostClicks } from '../../base.js';
|
|
2
2
|
|
|
3
3
|
// ── Grouping ──
|
|
4
4
|
// Every <glk-radio> keeps its <input type="radio"> in its own shadow root, and
|
|
@@ -29,9 +29,11 @@ function syncGroupTabIndex(group) {
|
|
|
29
29
|
|
|
30
30
|
class GlkRadio extends GlkFormElement {
|
|
31
31
|
static get observedAttributes() {
|
|
32
|
-
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
32
|
+
return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
static get hostStyles() { return checkControlSheet; }
|
|
36
|
+
|
|
35
37
|
render() {
|
|
36
38
|
const label = this.createElement('label', ['glass-radio']);
|
|
37
39
|
|
|
@@ -86,11 +88,13 @@ class GlkRadio extends GlkFormElement {
|
|
|
86
88
|
|
|
87
89
|
this._input.addEventListener('change', this._onChange);
|
|
88
90
|
this._input.addEventListener('keydown', this._onKeyDown);
|
|
91
|
+
this._unforwardClicks = forwardHostClicks(this, () => this._input);
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
teardownEvents() {
|
|
92
95
|
this._input?.removeEventListener('change', this._onChange);
|
|
93
96
|
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
97
|
+
this._unforwardClicks?.();
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
@@ -125,6 +129,26 @@ class GlkRadio extends GlkFormElement {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
get _validityField() { return this._input; }
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Radios are missing a value as a group: one required radio makes the
|
|
136
|
+
* whole group required, and any checked one satisfies it. Each input here
|
|
137
|
+
* sits alone in its shadow root, so the group's verdict is worked out across
|
|
138
|
+
* the elements and handed to every member. The inner input carries
|
|
139
|
+
* `required` only while the group lacks a value — that way it produces the
|
|
140
|
+
* browser's own message for it.
|
|
141
|
+
*/
|
|
142
|
+
syncValidity() {
|
|
143
|
+
const group = this._group();
|
|
144
|
+
const missing = group.some(el => el.hasAttribute('required')) && !group.some(el => el.checked);
|
|
145
|
+
for (const el of group) {
|
|
146
|
+
if (!el._input) continue; // not rendered yet; syncs when it is
|
|
147
|
+
el._input.required = missing;
|
|
148
|
+
super.syncValidity.call(el);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
onAttributeChanged(name) {
|
|
129
153
|
if (this._syncing) return;
|
|
130
154
|
if (!this._input) return;
|
|
@@ -10,7 +10,8 @@ class GlkRange extends GlkFormElement {
|
|
|
10
10
|
|
|
11
11
|
const header = this.createElement('div', ['glass-range-header']);
|
|
12
12
|
|
|
13
|
-
this
|
|
13
|
+
// Tied to the slider by an id that only has to be unique in this shadow root.
|
|
14
|
+
this._labelEl = this.createElement('label', [], { for: 'field' });
|
|
14
15
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
15
16
|
|
|
16
17
|
this._valueEl = this.createElement('span', ['glass-range-value']);
|
|
@@ -19,6 +20,7 @@ class GlkRange extends GlkFormElement {
|
|
|
19
20
|
header.appendChild(this._valueEl);
|
|
20
21
|
|
|
21
22
|
this._input = this.createElement('input', ['glass-range'], {
|
|
23
|
+
id: 'field',
|
|
22
24
|
type: 'range',
|
|
23
25
|
min: this.getAttribute('min') || '0',
|
|
24
26
|
max: this.getAttribute('max') || '100',
|
|
@@ -4,7 +4,7 @@ const SEARCH_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
|
|
4
4
|
|
|
5
5
|
class GlkSearch extends GlkFormElement {
|
|
6
6
|
static get observedAttributes() {
|
|
7
|
-
return ['placeholder', 'name', 'value', 'disabled'];
|
|
7
|
+
return ['placeholder', 'name', 'value', 'disabled', 'label'];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
render() {
|
|
@@ -20,6 +20,10 @@ class GlkSearch extends GlkFormElement {
|
|
|
20
20
|
const placeholder = this.getAttribute('placeholder');
|
|
21
21
|
if (placeholder) this._input.setAttribute('placeholder', placeholder);
|
|
22
22
|
|
|
23
|
+
// The search field has no visible label; `label` names it for assistive
|
|
24
|
+
// technology. Without it the placeholder is all a screen reader can say.
|
|
25
|
+
this._applyLabel();
|
|
26
|
+
|
|
23
27
|
const name = this.getAttribute('name');
|
|
24
28
|
if (name) this._input.setAttribute('name', name);
|
|
25
29
|
|
|
@@ -64,9 +68,18 @@ class GlkSearch extends GlkFormElement {
|
|
|
64
68
|
case 'disabled':
|
|
65
69
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
66
70
|
break;
|
|
71
|
+
case 'label':
|
|
72
|
+
this._applyLabel();
|
|
73
|
+
break;
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
_applyLabel() {
|
|
78
|
+
const label = this.getAttribute('label');
|
|
79
|
+
if (label) this._input.setAttribute('aria-label', label);
|
|
80
|
+
else this._input.removeAttribute('aria-label');
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
_syncFormValue() {
|
|
71
84
|
this.setFormValue(this._input.value);
|
|
72
85
|
}
|
|
@@ -39,6 +39,10 @@ function parseList(json) {
|
|
|
39
39
|
class GlkSegmented extends GlkFormElement {
|
|
40
40
|
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
|
|
41
41
|
|
|
42
|
+
// A row of buttons with one tab stop: delegated focus would land on the
|
|
43
|
+
// first button, not the chosen one.
|
|
44
|
+
static get delegatesFocus() { return false; }
|
|
45
|
+
|
|
42
46
|
render() {
|
|
43
47
|
this._group = this.createElement('div', ['glass-segmented'], { role: 'group', part: 'group' });
|
|
44
48
|
this._wrapper.appendChild(this._group);
|
|
@@ -10,10 +10,11 @@ class GlkSelect extends GlkFormElement {
|
|
|
10
10
|
render() {
|
|
11
11
|
const group = this.createElement('div', ['glass-input-group']);
|
|
12
12
|
|
|
13
|
-
this
|
|
13
|
+
// Tied to the field by an id that only has to be unique in this shadow root.
|
|
14
|
+
this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
|
|
14
15
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
15
16
|
|
|
16
|
-
this._select = this.createElement('select', ['glass-select']);
|
|
17
|
+
this._select = this.createElement('select', ['glass-select'], { id: 'field' });
|
|
17
18
|
|
|
18
19
|
const name = this.getAttribute('name');
|
|
19
20
|
if (name) this._select.setAttribute('name', name);
|
|
@@ -101,9 +102,14 @@ class GlkSelect extends GlkFormElement {
|
|
|
101
102
|
this._applyValue(this.getAttribute('value'));
|
|
102
103
|
this._syncFormValue();
|
|
103
104
|
break;
|
|
105
|
+
case 'required':
|
|
106
|
+
this._select.required = this.getBoolAttr('required');
|
|
107
|
+
break;
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
get _validityField() { return this._select; }
|
|
112
|
+
|
|
107
113
|
_syncFormValue() {
|
|
108
114
|
this.setFormValue(this._select.value);
|
|
109
115
|
}
|
|
@@ -8,10 +8,11 @@ class GlkTextarea extends GlkFormElement {
|
|
|
8
8
|
render() {
|
|
9
9
|
const group = this.createElement('div', ['glass-input-group']);
|
|
10
10
|
|
|
11
|
-
this
|
|
11
|
+
// Tied to the field by an id that only has to be unique in this shadow root.
|
|
12
|
+
this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
|
|
12
13
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
13
14
|
|
|
14
|
-
this._textarea = this.createElement('textarea', ['glass-textarea']);
|
|
15
|
+
this._textarea = this.createElement('textarea', ['glass-textarea'], { id: 'field' });
|
|
15
16
|
const placeholder = this.getAttribute('placeholder');
|
|
16
17
|
if (placeholder) this._textarea.setAttribute('placeholder', placeholder);
|
|
17
18
|
|
|
@@ -69,9 +70,14 @@ class GlkTextarea extends GlkFormElement {
|
|
|
69
70
|
this._textarea.value = this.getAttribute('value') || '';
|
|
70
71
|
this._syncFormValue();
|
|
71
72
|
break;
|
|
73
|
+
case 'required':
|
|
74
|
+
this._textarea.required = this.getBoolAttr('required');
|
|
75
|
+
break;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
get _validityField() { return this._textarea; }
|
|
80
|
+
|
|
75
81
|
_syncFormValue() {
|
|
76
82
|
this.setFormValue(this._textarea.value);
|
|
77
83
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { GlkFormElement } from '../../base.js';
|
|
1
|
+
import { GlkFormElement, checkControlSheet, forwardHostClicks } from '../../base.js';
|
|
2
2
|
|
|
3
3
|
class GlkToggle extends GlkFormElement {
|
|
4
4
|
static get observedAttributes() {
|
|
5
|
-
return ['checked', 'disabled', 'label', 'name', 'value'];
|
|
5
|
+
return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
static get hostStyles() { return checkControlSheet; }
|
|
9
|
+
|
|
8
10
|
render() {
|
|
9
11
|
const label = this.createElement('label', ['glass-toggle']);
|
|
10
12
|
|
|
13
|
+
// The native checkbox is the switch: role, checked state and name (from
|
|
14
|
+
// the label around it) all sit on one control. The host has no role of
|
|
15
|
+
// its own — it used to carry role="switch" as well, and screen readers met
|
|
16
|
+
// a switch with a checkbox inside.
|
|
11
17
|
this._input = this.createElement('input', ['glass-toggle__input'], {
|
|
12
|
-
type: 'checkbox'
|
|
18
|
+
type: 'checkbox',
|
|
19
|
+
role: 'switch'
|
|
13
20
|
});
|
|
14
21
|
|
|
15
22
|
const name = this.getAttribute('name');
|
|
@@ -28,14 +35,12 @@ class GlkToggle extends GlkFormElement {
|
|
|
28
35
|
|
|
29
36
|
if (this.getBoolAttr('checked')) this._input.checked = true;
|
|
30
37
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
38
|
+
if (this.getBoolAttr('required')) this._input.required = true;
|
|
31
39
|
|
|
32
40
|
this._defaultChecked = this.getBoolAttr('checked');
|
|
33
41
|
this._wrapper.appendChild(label);
|
|
34
42
|
|
|
35
43
|
this._syncFormValue();
|
|
36
|
-
this.setAttribute('role', 'switch');
|
|
37
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
38
|
-
if (this.getBoolAttr('disabled')) this.setAttribute('aria-disabled', 'true');
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
setupEvents() {
|
|
@@ -43,16 +48,17 @@ class GlkToggle extends GlkFormElement {
|
|
|
43
48
|
this._syncing = true;
|
|
44
49
|
this.setBoolAttr('checked', this._input.checked);
|
|
45
50
|
this._syncing = false;
|
|
46
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
47
51
|
this._syncFormValue();
|
|
48
52
|
this.emit('glk-change', { checked: this._input.checked });
|
|
49
53
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
50
54
|
};
|
|
51
55
|
this._input.addEventListener('change', this._onChange);
|
|
56
|
+
this._unforwardClicks = forwardHostClicks(this, () => this._input);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
teardownEvents() {
|
|
55
60
|
this._input?.removeEventListener('change', this._onChange);
|
|
61
|
+
this._unforwardClicks?.();
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
onAttributeChanged(name) {
|
|
@@ -61,12 +67,10 @@ class GlkToggle extends GlkFormElement {
|
|
|
61
67
|
switch (name) {
|
|
62
68
|
case 'checked':
|
|
63
69
|
this._input.checked = this.getBoolAttr('checked');
|
|
64
|
-
this.setAttribute('aria-checked', String(this._input.checked));
|
|
65
70
|
this._syncFormValue();
|
|
66
71
|
break;
|
|
67
72
|
case 'disabled':
|
|
68
73
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
69
|
-
this.setAttribute('aria-disabled', String(this.getBoolAttr('disabled')));
|
|
70
74
|
break;
|
|
71
75
|
case 'label':
|
|
72
76
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
@@ -74,9 +78,14 @@ class GlkToggle extends GlkFormElement {
|
|
|
74
78
|
case 'name':
|
|
75
79
|
this._input.setAttribute('name', this.getAttribute('name') || '');
|
|
76
80
|
break;
|
|
81
|
+
case 'required':
|
|
82
|
+
this._input.required = this.getBoolAttr('required');
|
|
83
|
+
break;
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
get _validityField() { return this._input; }
|
|
88
|
+
|
|
80
89
|
_syncFormValue() {
|
|
81
90
|
const val = this.getAttribute('value') || 'on';
|
|
82
91
|
this.setFormValue(this._input.checked ? val : null);
|
|
@@ -85,7 +94,6 @@ class GlkToggle extends GlkFormElement {
|
|
|
85
94
|
resetValue() {
|
|
86
95
|
this._input.checked = this._defaultChecked;
|
|
87
96
|
this.setBoolAttr('checked', this._defaultChecked);
|
|
88
|
-
this.setAttribute('aria-checked', String(this._defaultChecked));
|
|
89
97
|
this._syncFormValue();
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -100,7 +108,6 @@ class GlkToggle extends GlkFormElement {
|
|
|
100
108
|
set checked(v) {
|
|
101
109
|
if (this._input) this._input.checked = v;
|
|
102
110
|
this.setBoolAttr('checked', v);
|
|
103
|
-
this.setAttribute('aria-checked', String(v));
|
|
104
111
|
this._syncFormValue();
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { GlkElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
+
// Handed down to the native button, which is what assistive technology reads —
|
|
4
|
+
// <glk-popover> sets aria-expanded on its trigger.
|
|
5
|
+
const FORWARDED_ARIA = ['aria-expanded', 'aria-haspopup', 'aria-pressed'];
|
|
6
|
+
|
|
3
7
|
class GlkPill extends GlkElement {
|
|
4
8
|
static get observedAttributes() {
|
|
5
|
-
return ['label', 'disabled'];
|
|
9
|
+
return ['label', 'disabled', ...FORWARDED_ARIA];
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
render() {
|
|
@@ -12,6 +16,7 @@ class GlkPill extends GlkElement {
|
|
|
12
16
|
|
|
13
17
|
const ariaLabel = this.getAttribute('label');
|
|
14
18
|
if (ariaLabel) this._btn.setAttribute('aria-label', ariaLabel);
|
|
19
|
+
for (const attr of FORWARDED_ARIA) this._forward(attr);
|
|
15
20
|
|
|
16
21
|
// Slotted content (SVG icons, text)
|
|
17
22
|
this._btn.appendChild(document.createElement('slot'));
|
|
@@ -40,8 +45,16 @@ class GlkPill extends GlkElement {
|
|
|
40
45
|
case 'disabled':
|
|
41
46
|
this._btn.disabled = this.getBoolAttr('disabled');
|
|
42
47
|
break;
|
|
48
|
+
default:
|
|
49
|
+
if (FORWARDED_ARIA.includes(name)) this._forward(name);
|
|
43
50
|
}
|
|
44
51
|
}
|
|
52
|
+
|
|
53
|
+
_forward(attr) {
|
|
54
|
+
const value = this.getAttribute(attr);
|
|
55
|
+
if (value === null) this._btn.removeAttribute(attr);
|
|
56
|
+
else this._btn.setAttribute(attr, value);
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
customElements.define('glk-pill', GlkPill);
|