@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +40 -27
  3. package/SKILL.md +107 -25
  4. package/dist/components/base.js +145 -10
  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 +7 -4
  8. package/dist/components/glk-checkbox.js +12 -2
  9. package/dist/components/glk-date-strip.js +5 -4
  10. package/dist/components/glk-input.js +25 -4
  11. package/dist/components/glk-modal.js +99 -21
  12. package/dist/components/glk-pill.js +14 -1
  13. package/dist/components/glk-popover.js +21 -3
  14. package/dist/components/glk-radio.js +26 -2
  15. package/dist/components/glk-range.js +3 -1
  16. package/dist/components/glk-search.js +14 -1
  17. package/dist/components/glk-segmented.js +4 -0
  18. package/dist/components/glk-select.js +8 -2
  19. package/dist/components/glk-textarea.js +8 -2
  20. package/dist/components/glk-toggle.js +18 -11
  21. package/dist/components/shared/{dates-CNWaJf09.js → dates-D4JTqoiG.js} +25 -4
  22. package/dist/glasskit-elements.esm.js +519 -75
  23. package/dist/glasskit-elements.js +519 -75
  24. package/dist/glasskit-elements.min.js +1 -1
  25. package/package.json +3 -3
  26. package/src/base.js +144 -9
  27. package/src/components/buttons/glk-button.js +69 -2
  28. package/src/components/containers/glk-accordion-item.js +20 -5
  29. package/src/components/feedback/glk-modal.js +99 -21
  30. package/src/components/feedback/glk-popover.js +21 -3
  31. package/src/components/forms/glk-calendar.js +6 -3
  32. package/src/components/forms/glk-checkbox.js +12 -2
  33. package/src/components/forms/glk-input.js +25 -4
  34. package/src/components/forms/glk-radio.js +26 -2
  35. package/src/components/forms/glk-range.js +3 -1
  36. package/src/components/forms/glk-search.js +14 -1
  37. package/src/components/forms/glk-segmented.js +4 -0
  38. package/src/components/forms/glk-select.js +8 -2
  39. package/src/components/forms/glk-textarea.js +8 -2
  40. package/src/components/forms/glk-toggle.js +18 -11
  41. package/src/components/navigation/glk-date-strip.js +4 -3
  42. package/src/components/navigation/glk-pill.js +14 -1
  43. package/src/dates.js +25 -4
@@ -11,18 +11,44 @@ import { tokensCss, componentsSheet } from '@jungherz-de/glasskit/glasskit-style
11
11
  // inherits them like any other custom property. They are wrapped in a cascade
12
12
  // layer so an ordinary (unlayered) brand stylesheet wins over them, no matter
13
13
  // whether it loads before or after this module.
14
+ //
15
+ // Only the custom properties go there. GlassKit's token blocks also set
16
+ // color-scheme, and on the document that is no default: it switches the whole
17
+ // page to dark — native controls, scrollbars, the root text colour, link
18
+ // colours — on pages that never asked for GlassKit's theme. The elements set
19
+ // color-scheme on their own theme wrapper instead (host sheet below).
20
+ //
21
+ // The --gl-* names themselves are still global. A page that declares them
22
+ // itself, or wants them on part of the page only, switches the defaults off
23
+ // with <html data-glk-defaults="off">. The attribute is watched, so it may
24
+ // also be set or removed later.
14
25
 
15
26
  const TOKENS_INJECTED = '__glkDefaultTokensInjected';
27
+ const DEFAULTS_ATTR = 'data-glk-defaults';
28
+ let defaultTokensSheet = null;
16
29
 
17
30
  function injectDefaultTokens() {
18
31
  if (typeof document === 'undefined') return; // SSR / non-DOM
19
32
  if (globalThis[TOKENS_INJECTED]) return; // another bundle copy did it
20
33
  globalThis[TOKENS_INJECTED] = true;
21
34
 
22
- const sheet = new CSSStyleSheet();
23
- sheet.replaceSync(`@layer glasskit-defaults { ${tokensCss} }`);
24
- // Append — never assign — so an app's own adopted sheets survive.
25
- document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
35
+ const tokens = tokensCss.replace(/color-scheme\s*:[^;}]*;?/g, '');
36
+ defaultTokensSheet = new CSSStyleSheet();
37
+ defaultTokensSheet.replaceSync(`@layer glasskit-defaults { ${tokens} }`);
38
+ syncDefaultTokens();
39
+ }
40
+
41
+ /** Adopts or drops the default tokens to match <html data-glk-defaults>. */
42
+ function syncDefaultTokens() {
43
+ if (!defaultTokensSheet) return;
44
+ const wanted = document.documentElement.getAttribute(DEFAULTS_ATTR) !== 'off';
45
+ const sheets = document.adoptedStyleSheets;
46
+ if (wanted === sheets.includes(defaultTokensSheet)) return;
47
+ // Add or remove only this sheet — never assign a fresh list — so an app's
48
+ // own adopted sheets survive.
49
+ document.adoptedStyleSheets = wanted
50
+ ? [...sheets, defaultTokensSheet]
51
+ : sheets.filter(sheet => sheet !== defaultTokensSheet);
26
52
  }
27
53
 
28
54
  injectDefaultTokens();
@@ -45,31 +71,94 @@ function syncAllThemes() {
45
71
  }
46
72
 
47
73
  if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
48
- const observer = new MutationObserver(syncAllThemes);
74
+ const observer = new MutationObserver(records => {
75
+ if (records.some(r => r.attributeName === 'data-theme')) syncAllThemes();
76
+ if (records.some(r => r.attributeName === DEFAULTS_ATTR)) syncDefaultTokens();
77
+ });
49
78
  observer.observe(document.documentElement, {
50
79
  attributes: true,
51
- attributeFilter: ['data-theme']
80
+ attributeFilter: ['data-theme', DEFAULTS_ATTR]
52
81
  });
53
82
  }
54
83
 
55
84
  // ── Host Stylesheet ──
56
85
  // Sets display:block on all custom elements by default.
57
86
  // Inline components (badge, avatar) override this.
87
+ //
88
+ // The wrapper carries the theme's color-scheme, so native parts inside the
89
+ // elements — a select's list, a date picker, scrollbars — match the element,
90
+ // whatever the page around it uses.
91
+
92
+ const wrapperRules = `
93
+ .glk-wrapper { display: contents; }
94
+ .glk-wrapper[data-theme="dark"] { color-scheme: dark; }
95
+ .glk-wrapper[data-theme="light"] { color-scheme: light; }
96
+ `;
58
97
 
59
98
  const hostSheet = new CSSStyleSheet();
60
99
  hostSheet.replaceSync(`
61
100
  :host { display: block; }
62
101
  :host([hidden]) { display: none; }
63
- .glk-wrapper { display: contents; }
102
+ ${wrapperRules}
64
103
  `);
65
104
 
66
105
  const inlineHostSheet = new CSSStyleSheet();
67
106
  inlineHostSheet.replaceSync(`
68
107
  :host { display: inline-block; }
69
108
  :host([hidden]) { display: none; }
70
- .glk-wrapper { display: contents; }
109
+ ${wrapperRules}
110
+ `);
111
+
112
+ /**
113
+ * Host rules for glk-checkbox, glk-radio and glk-toggle. Their control is an
114
+ * inline-flex label inside a block host; sitting on the text baseline it left
115
+ * room for descenders below, so the host was about 4px taller than the
116
+ * control and the control sat high in a centred row. Aligned to the top of
117
+ * its line, the label is as tall as the host.
118
+ */
119
+ const checkControlSheet = new CSSStyleSheet();
120
+ checkControlSheet.replaceSync(`
121
+ .glass-checkbox, .glass-radio, .glass-toggle { vertical-align: top; }
71
122
  `);
72
123
 
124
+ // ── Clicks Aimed at the Host ──
125
+ // The innermost target of the latest pointerdown anywhere in the document;
126
+ // tracked once, on first use.
127
+ let lastPressed = null;
128
+ let trackingPresses = false;
129
+
130
+ /**
131
+ * For elements whose control is a native input or button inside the shadow
132
+ * root: glk-checkbox, glk-radio, glk-toggle, glk-button. A click aimed at the
133
+ * host itself — host.click(), or the click a <label> around the element or
134
+ * naming it dispatches — reaches only the host. The control never saw it, so
135
+ * nothing changed where a native checkbox toggles and a native button
136
+ * submits. Such a click is handed to the control; the original is stopped, so
137
+ * listeners see one click, the control's.
138
+ *
139
+ * A pointer press on the host's own empty box (the host is a block, the
140
+ * control is narrower) also lands on the host. Chrome cannot tell it apart
141
+ * from a label's click by the event, only by where the pointer went down; it
142
+ * stays a no-op, as before. Returns the teardown.
143
+ */
144
+ function forwardHostClicks(host, getField) {
145
+ if (!trackingPresses && typeof window !== 'undefined') {
146
+ trackingPresses = true;
147
+ window.addEventListener('pointerdown', e => { lastPressed = e.composedPath()[0]; },
148
+ { capture: true, passive: true });
149
+ }
150
+ const onClick = (e) => {
151
+ if (e.composedPath()[0] !== host) return; // from inside: the input had it
152
+ if (e.isTrusted && lastPressed === host) return; // a press on the empty box
153
+ const field = getField();
154
+ if (!field || field.disabled) return;
155
+ e.stopImmediatePropagation();
156
+ field.click();
157
+ };
158
+ host.addEventListener('click', onClick);
159
+ return () => host.removeEventListener('click', onClick);
160
+ }
161
+
73
162
  // ── Base Class ──
74
163
 
75
164
  class GlkElement extends HTMLElement {
@@ -92,6 +181,13 @@ class GlkElement extends HTMLElement {
92
181
  */
93
182
  static get observesLightDom() { return false; }
94
183
 
184
+ /**
185
+ * Turned on by elements built around one native field: focus() on the
186
+ * host, a click on the label text and a <label for> outside all land in
187
+ * that field.
188
+ */
189
+ static get delegatesFocus() { return false; }
190
+
95
191
  static get observedAttributes() {
96
192
  return [];
97
193
  }
@@ -99,7 +195,7 @@ class GlkElement extends HTMLElement {
99
195
  constructor() {
100
196
  super();
101
197
  this._initialized = false;
102
- this._shadow = this.attachShadow({ mode: 'open' });
198
+ this._shadow = this.attachShadow({ mode: 'open', delegatesFocus: this.constructor.delegatesFocus });
103
199
  const displaySheet = this.constructor.displayInline ? inlineHostSheet : hostSheet;
104
200
  const sheets = [componentsSheet, displaySheet];
105
201
  const extra = this.constructor.hostStyles;
@@ -240,14 +336,26 @@ class GlkFormElement extends GlkElement {
240
336
 
241
337
  static formAssociated = true;
242
338
 
339
+ // One native field inside, so focus goes there. The button groups
340
+ // (glk-segmented, glk-calendar) turn this off again: their first button is
341
+ // not the chosen one.
342
+ static get delegatesFocus() { return true; }
343
+
243
344
  constructor() {
244
345
  super();
245
346
  this._internals = this.attachInternals();
246
347
  }
247
348
 
349
+ attributeChangedCallback(name, oldValue, newValue) {
350
+ super.attributeChangedCallback(name, oldValue, newValue);
351
+ // required, min, pattern, type … all change what counts as valid.
352
+ if (this._initialized) this.syncValidity();
353
+ }
354
+
248
355
  get form() { return this._internals.form; }
249
356
  get validationMessage() { return this._internals.validationMessage; }
250
357
  get validity() { return this._internals.validity; }
358
+ get willValidate() { return this._internals.willValidate; }
251
359
 
252
360
  checkValidity() { return this._internals.checkValidity(); }
253
361
  reportValidity() { return this._internals.reportValidity(); }
@@ -266,11 +374,38 @@ class GlkFormElement extends GlkElement {
266
374
 
267
375
  setFormValue(value) {
268
376
  this._internals.setFormValue(value);
377
+ this.syncValidity();
269
378
  }
270
379
 
271
380
  setValidity(flags, message, anchor) {
272
381
  this._internals.setValidity(flags, message, anchor);
273
382
  }
383
+
384
+ /**
385
+ * The native field whose constraint validation the host reports, or null.
386
+ * Subclasses built around one field return it.
387
+ */
388
+ get _validityField() { return null; }
389
+
390
+ /**
391
+ * A form asks the host — checkValidity(), reportValidity(), submitting — and
392
+ * the host knows nothing by itself, so the inner field's verdict is copied
393
+ * over after everything that can change it: a value (every setFormValue
394
+ * lands here), an attribute, a reset. The field is the anchor: the browser's
395
+ * bubble points at it, and reportValidity() focuses it.
396
+ */
397
+ syncValidity() {
398
+ const field = this._validityField;
399
+ if (!field) return;
400
+ const message = field.validationMessage;
401
+ // A field barred from validation (disabled) can still carry flags, but
402
+ // it has no message, and setValidity() refuses flags without one.
403
+ if (!field.willValidate || field.validity.valid || !message) {
404
+ this._internals.setValidity({});
405
+ } else {
406
+ this._internals.setValidity(field.validity, message, field);
407
+ }
408
+ }
274
409
  }
275
410
 
276
- export { GlkElement, GlkFormElement };
411
+ export { GlkElement, GlkFormElement, checkControlSheet, forwardHostClicks };
@@ -10,17 +10,19 @@ class GlkAccordionItem extends GlkElement {
10
10
 
11
11
  render() {
12
12
  this._item = this.createElement('div', ['glass-accordion__item']);
13
- if (this.getBoolAttr('open')) this._item.classList.add('is-open');
14
13
 
15
- // Trigger button
16
- this._trigger = this.createElement('button', ['glass-accordion__trigger']);
14
+ // Trigger button — says whether its section is open, and which one it is
15
+ this._trigger = this.createElement('button', ['glass-accordion__trigger'], {
16
+ type: 'button',
17
+ 'aria-controls': 'content'
18
+ });
17
19
  this.takeTitle();
18
20
  this._triggerText = document.createTextNode(this._title || '');
19
21
  this._trigger.appendChild(this._triggerText);
20
22
  this._trigger.insertAdjacentHTML('beforeend', CHEVRON_SVG);
21
23
 
22
24
  // Content
23
- this._content = this.createElement('div', ['glass-accordion__content']);
25
+ this._content = this.createElement('div', ['glass-accordion__content'], { id: 'content' });
24
26
  const body = this.createElement('div', ['glass-accordion__body']);
25
27
  body.appendChild(document.createElement('slot'));
26
28
  this._content.appendChild(body);
@@ -29,6 +31,19 @@ class GlkAccordionItem extends GlkElement {
29
31
  this._item.appendChild(this._content);
30
32
 
31
33
  this._wrapper.appendChild(this._item);
34
+ this._applyOpen();
35
+ }
36
+
37
+ /**
38
+ * A closed section is only squeezed to zero height; its links and fields
39
+ * stayed in the tab order. inert takes them out, and out of the
40
+ * accessibility tree, until the section opens.
41
+ */
42
+ _applyOpen() {
43
+ const open = this.getBoolAttr('open');
44
+ this._item.classList.toggle('is-open', open);
45
+ this._trigger.setAttribute('aria-expanded', String(open));
46
+ this._content.inert = !open;
32
47
  }
33
48
 
34
49
  setupEvents() {
@@ -47,7 +62,7 @@ class GlkAccordionItem extends GlkElement {
47
62
  if (!this._item) return;
48
63
  switch (name) {
49
64
  case 'open':
50
- this._item.classList.toggle('is-open', this.getBoolAttr('open'));
65
+ this._applyOpen();
51
66
  break;
52
67
  case 'title':
53
68
  if (this.takeTitle(value)) this._applyTitle();
@@ -1,12 +1,27 @@
1
- import { GlkElement } from './base.js';
1
+ import { GlkElement, forwardHostClicks } from './base.js';
2
2
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
4
  const VARIANTS = ['primary', 'secondary', 'tertiary'];
5
5
  const SIZES = ['sm', 'md', 'lg', 'auto'];
6
6
 
7
+ // States that whoever owns the element sets on it — <glk-popover> sets
8
+ // aria-expanded on its trigger. Assistive technology reads the native button
9
+ // inside, so they are handed down to it.
10
+ const FORWARDED_ARIA = ['aria-expanded', 'aria-haspopup', 'aria-pressed'];
11
+
7
12
  class GlkButton extends GlkElement {
8
13
  static get observedAttributes() {
9
- return ['variant', 'size', 'disabled', 'type'];
14
+ return ['variant', 'size', 'disabled', 'type', ...FORWARDED_ARIA];
15
+ }
16
+
17
+ // The native button sits in the shadow root, outside every form. Form
18
+ // association gives the element its form, so type="submit" and "reset"
19
+ // can act on it (see _activateForm).
20
+ static formAssociated = true;
21
+
22
+ constructor() {
23
+ super();
24
+ this._internals = this.attachInternals();
10
25
  }
11
26
 
12
27
  render() {
@@ -17,6 +32,7 @@ class GlkButton extends GlkElement {
17
32
  if (this.getBoolAttr('disabled')) {
18
33
  this._btn.disabled = true;
19
34
  }
35
+ for (const attr of FORWARDED_ARIA) this._forward(attr);
20
36
 
21
37
  this._btn.appendChild(document.createElement('slot'));
22
38
  this._wrapper.appendChild(this._btn);
@@ -30,12 +46,50 @@ class GlkButton extends GlkElement {
30
46
  return;
31
47
  }
32
48
  this.emit('glk-click');
49
+ this._activateForm(e);
33
50
  };
34
51
  this._btn.addEventListener('click', this._onClick);
52
+ // host.click() and a <label> for the element reach the host only.
53
+ this._unforwardClicks = forwardHostClicks(this, () => this._btn);
35
54
  }
36
55
 
37
56
  teardownEvents() {
38
57
  this._btn?.removeEventListener('click', this._onClick);
58
+ this._unforwardClicks?.();
59
+ }
60
+
61
+ /**
62
+ * type="submit" submits the element's form, type="reset" resets it. Both
63
+ * wait until the click has finished bubbling, so a listener that calls
64
+ * preventDefault() on the click stops them — as on a native button.
65
+ */
66
+ _activateForm(click) {
67
+ const type = this.getAttribute('type');
68
+ const form = this._internals.form;
69
+ if (!form || (type !== 'submit' && type !== 'reset')) return;
70
+ // Through the prototype: a control with id or name "reset" or
71
+ // "requestSubmit" in the form shadows the methods on the form itself.
72
+ const { reset, requestSubmit } = HTMLFormElement.prototype;
73
+ setTimeout(() => {
74
+ if (click.defaultPrevented || this._internals.form !== form) return;
75
+ if (type === 'reset') {
76
+ reset.call(form);
77
+ return;
78
+ }
79
+ // requestSubmit() accepts native buttons only as the submitter, so the
80
+ // submit event is told about this element instead. The listener goes
81
+ // again right away: a form that fails validation fires no submit event,
82
+ // and the next submit must not inherit this submitter.
83
+ const nameSubmitter = (e) => {
84
+ Object.defineProperty(e, 'submitter', { configurable: true, get: () => this });
85
+ };
86
+ form.addEventListener('submit', nameSubmitter, { capture: true });
87
+ try {
88
+ requestSubmit.call(form);
89
+ } finally {
90
+ form.removeEventListener('submit', nameSubmitter, { capture: true });
91
+ }
92
+ });
39
93
  }
40
94
 
41
95
  onAttributeChanged(name) {
@@ -51,9 +105,17 @@ class GlkButton extends GlkElement {
51
105
  case 'type':
52
106
  this._btn.setAttribute('type', this.getAttribute('type') || 'button');
53
107
  break;
108
+ default:
109
+ if (FORWARDED_ARIA.includes(name)) this._forward(name);
54
110
  }
55
111
  }
56
112
 
113
+ _forward(attr) {
114
+ const value = this.getAttribute(attr);
115
+ if (value === null) this._btn.removeAttribute(attr);
116
+ else this._btn.setAttribute(attr, value);
117
+ }
118
+
57
119
  _computeClasses() {
58
120
  const classes = ['glass-btn'];
59
121
  const variant = this.getAttribute('variant');
@@ -75,6 +137,11 @@ class GlkButton extends GlkElement {
75
137
 
76
138
  get disabled() { return this.getBoolAttr('disabled'); }
77
139
  set disabled(v) { this.setBoolAttr('disabled', v); }
140
+
141
+ get type() { return this.getAttribute('type') || 'button'; }
142
+ set type(v) { this.setAttribute('type', v); }
143
+
144
+ get form() { return this._internals.form; }
78
145
  }
79
146
 
80
147
  customElements.define('glk-button', GlkButton);
@@ -1,5 +1,5 @@
1
1
  import { GlkFormElement } from './base.js';
2
- import { i as isoDate, r as resolveLocale, f as formatters, p as parseIso, a as firstWeekday, b as parseJsonObject, w as weekdayShort, c as addDays, d as isTone } from './shared/dates-CNWaJf09.js';
2
+ import { i as isoDate, r as resolveLocale, f as formatters, p as parseIso, a as firstWeekday, b as parseJsonObject, w as weekdayShort, c as addDays, d as isTone } from './shared/dates-D4JTqoiG.js';
3
3
  import '@jungherz-de/glasskit/glasskit-styles.js';
4
4
 
5
5
  // One month with a day to pick. Came back from EhrenPfoten in 1.16.0.
@@ -8,7 +8,7 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
8
8
  // today), value (YYYY-MM-DD), today (YYYY-MM-DD), min, max
9
9
  // (YYYY-MM-DD; days outside cannot be picked), marks (JSON
10
10
  // {date: tone | [tone, …]}, up to three dots), locale (default
11
- // the browser language), week-start (0 = Sunday … 6 = Saturday;
11
+ // the page language, else the browser's), week-start (0 = Sunday … 6 = Saturday;
12
12
  // default from the locale, Monday where the browser cannot say),
13
13
  // label (aria-label of the day group), prev-label / next-label
14
14
  // (names of the nav buttons, English by default)
@@ -46,6 +46,9 @@ class GlkCalendar extends GlkFormElement {
46
46
  return ['month', 'value', 'today', 'min', 'max', 'marks', 'locale', 'week-start', 'label', 'prev-label', 'next-label'];
47
47
  }
48
48
 
49
+ // Delegated focus would land on the month arrow, not on the chosen day.
50
+ static get delegatesFocus() { return false; }
51
+
49
52
  render() {
50
53
  this._root = this.createElement('div', ['glass-calendar'], { part: 'calendar' });
51
54
  const head = this.createElement('div', ['glass-calendar__head'], { part: 'head' });
@@ -66,7 +69,7 @@ class GlkCalendar extends GlkFormElement {
66
69
  get _todayIso() { return this.getAttribute('today') || isoDate(new Date()); }
67
70
 
68
71
  _build() {
69
- const locale = resolveLocale(this.getAttribute('locale'));
72
+ const locale = resolveLocale(this.getAttribute('locale'), this);
70
73
  const f = formatters(locale);
71
74
  const month = this.month;
72
75
  const first = parseIso(month);
@@ -243,7 +246,7 @@ class GlkCalendar extends GlkFormElement {
243
246
  get marks() { return parseJsonObject(this.getAttribute('marks')); }
244
247
  set marks(v) { this.setAttribute('marks', typeof v === 'string' ? v : JSON.stringify(v ?? {})); }
245
248
 
246
- get locale() { return resolveLocale(this.getAttribute('locale')); }
249
+ get locale() { return resolveLocale(this.getAttribute('locale'), this); }
247
250
  set locale(v) {
248
251
  if (v) this.setAttribute('locale', v);
249
252
  else this.removeAttribute('locale');
@@ -1,13 +1,15 @@
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
  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>`;
5
5
 
6
6
  class GlkCheckbox extends GlkFormElement {
7
7
  static get observedAttributes() {
8
- return ['checked', 'disabled', 'label', 'name', 'value'];
8
+ return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
9
9
  }
10
10
 
11
+ static get hostStyles() { return checkControlSheet; }
12
+
11
13
  render() {
12
14
  const label = this.createElement('label', ['glass-checkbox']);
13
15
 
@@ -30,6 +32,7 @@ class GlkCheckbox extends GlkFormElement {
30
32
 
31
33
  if (this.getBoolAttr('checked')) this._input.checked = true;
32
34
  if (this.getBoolAttr('disabled')) this._input.disabled = true;
35
+ if (this.getBoolAttr('required')) this._input.required = true;
33
36
 
34
37
  this._defaultChecked = this.getBoolAttr('checked');
35
38
  this._wrapper.appendChild(label);
@@ -46,10 +49,12 @@ class GlkCheckbox extends GlkFormElement {
46
49
  this.dispatchEvent(new Event('change', { bubbles: true }));
47
50
  };
48
51
  this._input.addEventListener('change', this._onChange);
52
+ this._unforwardClicks = forwardHostClicks(this, () => this._input);
49
53
  }
50
54
 
51
55
  teardownEvents() {
52
56
  this._input?.removeEventListener('change', this._onChange);
57
+ this._unforwardClicks?.();
53
58
  }
54
59
 
55
60
  onAttributeChanged(name) {
@@ -69,9 +74,14 @@ class GlkCheckbox extends GlkFormElement {
69
74
  case 'name':
70
75
  this._input.setAttribute('name', this.getAttribute('name') || '');
71
76
  break;
77
+ case 'required':
78
+ this._input.required = this.getBoolAttr('required');
79
+ break;
72
80
  }
73
81
  }
74
82
 
83
+ get _validityField() { return this._input; }
84
+
75
85
  _syncFormValue() {
76
86
  const val = this.getAttribute('value') || 'on';
77
87
  this.setFormValue(this._input.checked ? val : null);
@@ -1,5 +1,5 @@
1
1
  import { GlkElement } from './base.js';
2
- import { f as formatters, r as resolveLocale, b as parseJsonObject, p as parseIso, i as isoDate, c as addDays, w as weekdayShort, d as isTone } from './shared/dates-CNWaJf09.js';
2
+ import { f as formatters, r as resolveLocale, b as parseJsonObject, p as parseIso, i as isoDate, c as addDays, w as weekdayShort, d as isTone } from './shared/dates-D4JTqoiG.js';
3
3
  import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
4
4
  import '@jungherz-de/glasskit/glasskit-styles.js';
5
5
 
@@ -9,7 +9,8 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
9
9
  // Attributes: start (YYYY-MM-DD, default today), days (count, default 7),
10
10
  // value (YYYY-MM-DD), today (YYYY-MM-DD, default the browser's
11
11
  // day), marks (JSON {date: tone | {tone, disabled}}), locale
12
- // (default the browser language), label (aria-label of the group)
12
+ // (default the page language, else the browser's), label
13
+ // (aria-label of the group)
13
14
  // Properties: value, marks (object or JSON text), locale, label
14
15
  // Event: glk-change { value } — only on a change made by the user
15
16
  // Part: strip
@@ -42,7 +43,7 @@ class GlkDateStrip extends GlkElement {
42
43
  }
43
44
 
44
45
  _build() {
45
- const f = formatters(resolveLocale(this.getAttribute('locale')));
46
+ const f = formatters(resolveLocale(this.getAttribute('locale'), this));
46
47
  const marks = parseJsonObject(this.getAttribute('marks'));
47
48
  const start = parseIso(this.getAttribute('start')) ?? new Date();
48
49
  const today = this.getAttribute('today') || isoDate(new Date());
@@ -154,7 +155,7 @@ class GlkDateStrip extends GlkElement {
154
155
  get marks() { return parseJsonObject(this.getAttribute('marks')); }
155
156
  set marks(v) { this.setAttribute('marks', typeof v === 'string' ? v : JSON.stringify(v ?? {})); }
156
157
 
157
- get locale() { return resolveLocale(this.getAttribute('locale')); }
158
+ get locale() { return resolveLocale(this.getAttribute('locale'), this); }
158
159
  set locale(v) {
159
160
  if (v) this.setAttribute('locale', v);
160
161
  else this.removeAttribute('locale');
@@ -15,12 +15,15 @@ class GlkInput extends GlkFormElement {
15
15
  render() {
16
16
  const group = this.createElement('div', ['glass-input-group']);
17
17
 
18
- // Label
19
- this._labelEl = this.createElement('label', ['glass-label']);
18
+ // Label — tied to the field by id, which only has to be unique inside
19
+ // this shadow root. That gives the field its accessible name, and a click
20
+ // on the label text focuses it.
21
+ this._labelEl = this.createElement('label', ['glass-label'], { for: 'field' });
20
22
  this._labelEl.textContent = this.getAttribute('label') || '';
21
23
 
22
24
  // Input
23
25
  this._input = this.createElement('input', this._computeInputClasses(), {
26
+ id: 'field',
24
27
  type: this.getAttribute('type') || 'text'
25
28
  });
26
29
 
@@ -37,8 +40,8 @@ class GlkInput extends GlkFormElement {
37
40
  if (this.getBoolAttr('required')) this._input.required = true;
38
41
  for (const attr of FORWARDED) this._forward(attr);
39
42
 
40
- // Hint
41
- this._hintEl = this.createElement('span', this._computeHintClasses());
43
+ // Hint — read out with the field as its description
44
+ this._hintEl = this.createElement('span', this._computeHintClasses(), { id: 'hint' });
42
45
  this._hintEl.textContent = this.getAttribute('hint') || '';
43
46
 
44
47
  group.appendChild(this._labelEl);
@@ -48,6 +51,7 @@ class GlkInput extends GlkFormElement {
48
51
  this._group = group;
49
52
  this._wrapper.appendChild(group);
50
53
 
54
+ this._applyDescription();
51
55
  this._syncFormValue();
52
56
  }
53
57
 
@@ -85,12 +89,14 @@ class GlkInput extends GlkFormElement {
85
89
  case 'error':
86
90
  this._input.className = this._computeInputClasses().join(' ');
87
91
  this._hintEl.className = this._computeHintClasses().join(' ');
92
+ this._applyDescription();
88
93
  break;
89
94
  case 'hint':
90
95
  this._hintEl.textContent = this.getAttribute('hint') || '';
91
96
  if (this.getAttribute('hint') && !this._hintEl.parentNode) {
92
97
  this._group.appendChild(this._hintEl);
93
98
  }
99
+ this._applyDescription();
94
100
  break;
95
101
  case 'disabled':
96
102
  this._input.disabled = this.getBoolAttr('disabled');
@@ -110,6 +116,19 @@ class GlkInput extends GlkFormElement {
110
116
  }
111
117
  }
112
118
 
119
+ /**
120
+ * The hint describes the field; with `error` set it is the error message
121
+ * and the field is marked invalid. Server-side errors arrive this way, so
122
+ * `error` stays a marker for assistive technology and does not block the
123
+ * form — only the field's own constraints (required, pattern …) do.
124
+ */
125
+ _applyDescription() {
126
+ if (this.getAttribute('hint')) this._input.setAttribute('aria-describedby', 'hint');
127
+ else this._input.removeAttribute('aria-describedby');
128
+ if (this.getBoolAttr('error')) this._input.setAttribute('aria-invalid', 'true');
129
+ else this._input.removeAttribute('aria-invalid');
130
+ }
131
+
113
132
  _forward(attr) {
114
133
  const value = this.getAttribute(attr);
115
134
  if (value === null) this._input.removeAttribute(attr);
@@ -128,6 +147,8 @@ class GlkInput extends GlkFormElement {
128
147
  return classes;
129
148
  }
130
149
 
150
+ get _validityField() { return this._input; }
151
+
131
152
  _syncFormValue() {
132
153
  this.setFormValue(this._input.value);
133
154
  }