@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +56 -31
  3. package/SKILL.md +124 -27
  4. package/dist/components/base.js +185 -12
  5. package/dist/components/glk-accordion-item.js +20 -5
  6. package/dist/components/glk-button.js +69 -2
  7. package/dist/components/glk-calendar.js +3 -0
  8. package/dist/components/glk-checkbox.js +12 -2
  9. package/dist/components/glk-input.js +25 -4
  10. package/dist/components/glk-modal.js +99 -21
  11. package/dist/components/glk-pill.js +14 -1
  12. package/dist/components/glk-popover.js +21 -3
  13. package/dist/components/glk-radio.js +26 -2
  14. package/dist/components/glk-range.js +3 -1
  15. package/dist/components/glk-search.js +14 -1
  16. package/dist/components/glk-segmented.js +4 -0
  17. package/dist/components/glk-select.js +8 -2
  18. package/dist/components/glk-textarea.js +8 -2
  19. package/dist/components/glk-toggle.js +18 -11
  20. package/dist/glasskit-elements.esm.js +528 -68
  21. package/dist/glasskit-elements.js +528 -68
  22. package/dist/glasskit-elements.min.js +1 -1
  23. package/package.json +3 -3
  24. package/src/base.js +184 -11
  25. package/src/components/buttons/glk-button.js +69 -2
  26. package/src/components/containers/glk-accordion-item.js +20 -5
  27. package/src/components/feedback/glk-modal.js +99 -21
  28. package/src/components/feedback/glk-popover.js +21 -3
  29. package/src/components/forms/glk-calendar.js +3 -0
  30. package/src/components/forms/glk-checkbox.js +12 -2
  31. package/src/components/forms/glk-input.js +25 -4
  32. package/src/components/forms/glk-radio.js +26 -2
  33. package/src/components/forms/glk-range.js +3 -1
  34. package/src/components/forms/glk-search.js +14 -1
  35. package/src/components/forms/glk-segmented.js +4 -0
  36. package/src/components/forms/glk-select.js +8 -2
  37. package/src/components/forms/glk-textarea.js +8 -2
  38. package/src/components/forms/glk-toggle.js +18 -11
  39. 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
- const triggerSlot = document.createElement('slot');
14
- triggerSlot.setAttribute('name', 'trigger');
15
- this._anchor.appendChild(triggerSlot);
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._labelEl = this.createElement('label', ['glass-label']);
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._labelEl = this.createElement('label', []);
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._labelEl = this.createElement('label', ['glass-label']);
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._labelEl = this.createElement('label', ['glass-label']);
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);