@jungherz-de/glasskit-elements 1.19.1 → 1.20.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 +36 -0
- package/README.md +40 -27
- package/SKILL.md +107 -25
- package/dist/components/base.js +145 -10
- package/dist/components/glk-accordion-item.js +20 -5
- package/dist/components/glk-button.js +69 -2
- package/dist/components/glk-calendar.js +7 -4
- package/dist/components/glk-checkbox.js +12 -2
- package/dist/components/glk-date-strip.js +5 -4
- 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/components/shared/{dates-CNWaJf09.js → dates-D4JTqoiG.js} +25 -4
- package/dist/glasskit-elements.esm.js +519 -75
- package/dist/glasskit-elements.js +519 -75
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +144 -9
- 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 +6 -3
- 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-date-strip.js +4 -3
- package/src/components/navigation/glk-pill.js +14 -1
- package/src/dates.js +25 -4
|
@@ -7,7 +7,7 @@ import { isoDate, parseIso, addDays, parseJsonObject, resolveLocale, formatters,
|
|
|
7
7
|
// today), value (YYYY-MM-DD), today (YYYY-MM-DD), min, max
|
|
8
8
|
// (YYYY-MM-DD; days outside cannot be picked), marks (JSON
|
|
9
9
|
// {date: tone | [tone, …]}, up to three dots), locale (default
|
|
10
|
-
// the
|
|
10
|
+
// the page language, else the browser's), week-start (0 = Sunday … 6 = Saturday;
|
|
11
11
|
// default from the locale, Monday where the browser cannot say),
|
|
12
12
|
// label (aria-label of the day group), prev-label / next-label
|
|
13
13
|
// (names of the nav buttons, English by default)
|
|
@@ -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' });
|
|
@@ -65,7 +68,7 @@ class GlkCalendar extends GlkFormElement {
|
|
|
65
68
|
get _todayIso() { return this.getAttribute('today') || isoDate(new Date()); }
|
|
66
69
|
|
|
67
70
|
_build() {
|
|
68
|
-
const locale = resolveLocale(this.getAttribute('locale'));
|
|
71
|
+
const locale = resolveLocale(this.getAttribute('locale'), this);
|
|
69
72
|
const f = formatters(locale);
|
|
70
73
|
const month = this.month;
|
|
71
74
|
const first = parseIso(month);
|
|
@@ -242,7 +245,7 @@ class GlkCalendar extends GlkFormElement {
|
|
|
242
245
|
get marks() { return parseJsonObject(this.getAttribute('marks')); }
|
|
243
246
|
set marks(v) { this.setAttribute('marks', typeof v === 'string' ? v : JSON.stringify(v ?? {})); }
|
|
244
247
|
|
|
245
|
-
get locale() { return resolveLocale(this.getAttribute('locale')); }
|
|
248
|
+
get locale() { return resolveLocale(this.getAttribute('locale'), this); }
|
|
246
249
|
set locale(v) {
|
|
247
250
|
if (v) this.setAttribute('locale', v);
|
|
248
251
|
else this.removeAttribute('locale');
|
|
@@ -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
|
|
|
@@ -8,7 +8,8 @@ import { revealCentered } from '../../reveal.js';
|
|
|
8
8
|
// Attributes: start (YYYY-MM-DD, default today), days (count, default 7),
|
|
9
9
|
// value (YYYY-MM-DD), today (YYYY-MM-DD, default the browser's
|
|
10
10
|
// day), marks (JSON {date: tone | {tone, disabled}}), locale
|
|
11
|
-
// (default the
|
|
11
|
+
// (default the page language, else the browser's), label
|
|
12
|
+
// (aria-label of the group)
|
|
12
13
|
// Properties: value, marks (object or JSON text), locale, label
|
|
13
14
|
// Event: glk-change { value } — only on a change made by the user
|
|
14
15
|
// Part: strip
|
|
@@ -41,7 +42,7 @@ class GlkDateStrip extends GlkElement {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
_build() {
|
|
44
|
-
const f = formatters(resolveLocale(this.getAttribute('locale')));
|
|
45
|
+
const f = formatters(resolveLocale(this.getAttribute('locale'), this));
|
|
45
46
|
const marks = parseJsonObject(this.getAttribute('marks'));
|
|
46
47
|
const start = parseIso(this.getAttribute('start')) ?? new Date();
|
|
47
48
|
const today = this.getAttribute('today') || isoDate(new Date());
|
|
@@ -153,7 +154,7 @@ class GlkDateStrip extends GlkElement {
|
|
|
153
154
|
get marks() { return parseJsonObject(this.getAttribute('marks')); }
|
|
154
155
|
set marks(v) { this.setAttribute('marks', typeof v === 'string' ? v : JSON.stringify(v ?? {})); }
|
|
155
156
|
|
|
156
|
-
get locale() { return resolveLocale(this.getAttribute('locale')); }
|
|
157
|
+
get locale() { return resolveLocale(this.getAttribute('locale'), this); }
|
|
157
158
|
set locale(v) {
|
|
158
159
|
if (v) this.setAttribute('locale', v);
|
|
159
160
|
else this.removeAttribute('locale');
|
|
@@ -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);
|
package/src/dates.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Date helpers shared by <glk-date-strip> and <glk-calendar>. Everything
|
|
2
2
|
// works on local calendar days — a date is "YYYY-MM-DD" in the element API
|
|
3
3
|
// and a local Date inside — so a day never shifts across midnight the way a
|
|
4
|
-
// UTC timestamp would. Names come from Intl,
|
|
5
|
-
//
|
|
4
|
+
// UTC timestamp would. Names come from Intl, in the language resolveLocale()
|
|
5
|
+
// finds for the element.
|
|
6
6
|
|
|
7
7
|
export const pad = n => String(n).padStart(2, '0');
|
|
8
8
|
|
|
@@ -30,8 +30,29 @@ export function parseJsonObject(text) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The language to name days and months in: the element's locale attribute;
|
|
35
|
+
* else the language of the page around it — the lang of the nearest
|
|
36
|
+
* ancestor, looked up across shadow roots through their hosts, so
|
|
37
|
+
* <html lang="de"> reaches an element inside another component's shadow
|
|
38
|
+
* tree and a <section lang="en"> in a German page stays English; else the
|
|
39
|
+
* browser's; else English. An empty lang means "unknown" in HTML and falls
|
|
40
|
+
* through to the browser's language. (Since 1.19.2 — before, the browser's
|
|
41
|
+
* language came right after the attribute.)
|
|
42
|
+
*/
|
|
43
|
+
export function resolveLocale(attr, el) {
|
|
44
|
+
if (attr) return attr;
|
|
45
|
+
for (let node = el; node; ) {
|
|
46
|
+
const tagged = node.closest?.('[lang]');
|
|
47
|
+
if (tagged) {
|
|
48
|
+
const lang = tagged.getAttribute('lang').trim();
|
|
49
|
+
if (lang) return lang;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const root = node.getRootNode?.();
|
|
53
|
+
node = typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot ? root.host : null;
|
|
54
|
+
}
|
|
55
|
+
return (typeof navigator !== 'undefined' && navigator.language) || 'en';
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
// Intl formatters are cheap to keep and not to make; one set per locale.
|