@jungherz-de/glasskit-elements 1.22.1 → 1.22.3

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 CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.22.3] – 2026-10-08
11
+
12
+ GlassKit is unchanged; the peer dependency stays `>=1.22.2`.
13
+
14
+ ### Fixed
15
+
16
+ - **A `<fieldset disabled>` disables the form elements inside it.** The browser already counted them as disabled — `:disabled`, left out of `FormData` — but their native controls live in the shadow root, where no fieldset reaches, and stayed enabled: every field was reachable with Tab, typing and clicks changed the values, and `<glk-button type="submit">` fired `glk-click` and sent the form, empty. The elements now take the state from `formDisabledCallback`, which the browser calls for the element's own `disabled` attribute and for a fieldset alike, also when an element moves into or out of one. Inside a disabled fieldset they look disabled, drop out of the tab order, take no input and send nothing; a required empty field is barred from validation and counts again once the fieldset is enabled. The `disabled` property still reflects the attribute only, as on a native control. Radios skip a fieldset-disabled member of their group with the arrow keys, and the tab stop moves off it. ([#15](https://github.com/JUNGHERZ/GlassKit-Elements/issues/15))
17
+ - **`<glk-segmented disabled>` and `<glk-calendar disabled>` work.** Neither handled the attribute: the browser counted them as disabled and left them out of the form data, but a click still changed the value. Now the segmented row and the calendar's arrows and days are disabled; an option's own `disabled` stays as it is. ([#15](https://github.com/JUNGHERZ/GlassKit-Elements/issues/15))
18
+
19
+ ### Added
20
+
21
+ - **A `disabled` property on every form element.** `<glk-search>`, `<glk-range>`, `<glk-segmented>` and `<glk-calendar>` had none; it sits on `GlkFormElement` now. A subclass of `GlkFormElement` overrides `applyDisabled(disabled)` to hand the state to its native controls; it runs after the first render and on every change.
22
+
23
+ Measured in Chromium and WebKit with every form element inside `<form><fieldset disabled>`, 21 checks per engine: all native controls disabled with GlassKit's disabled look, Tab passes them, clicks and typing change nothing, no submit, no `glk-click`; enabled again, everything works and the button submits once; own attribute and fieldset combine in every order; an element moved in or created inside is disabled from the start; a calendar month shown while disabled stays disabled. Docs, showcase and landing pages render pixel-identical to 1.22.2.
24
+
25
+ ---
26
+
27
+ ## [1.22.2] – 2026-10-08
28
+
29
+ ### Fixed
30
+
31
+ - **In a `<glk-input>` affix, only what can be used takes the click.** Since 1.22.0 `::slotted(*)` gave pointer events back to everything slotted into `prefix` and `suffix`. A unit or an icon caught the click instead of the field. A disabled button took it, and focus delegation then moved the focus into the field — also into a read-only field whose container locks it with `pointer-events: none`, because the explicit `auto` outweighed the inherited `none`. Now the slots follow GlassKit: enabled controls, `<glk-button>`, `<glk-select>` and the other form elements included, links and elements with `tabindex` take the click; text, icons and disabled controls let it through to the field. A control has to sit in the slot itself: one wrapped in a slotted element needs `pointer-events: auto` from the page. A control passed on through another component's `<slot>` counts as slotted. Measured in Chromium and WebKit on the issue's page and 21 more cases: the unit, the icon and the disabled button hand the click to the field; in the locked container nothing reacts and the focus stays out; an enabled button, a link, a `<glk-button>` and a `<glk-select>` take their own. Docs, showcase and landing pages render pixel-identical to 1.22.1. Rebuilt against GlassKit 1.22.2, which applies the same rule to plain markup; the peer dependency moves to `>=1.22.2`. ([#14](https://github.com/JUNGHERZ/GlassKit-Elements/issues/14))
32
+
33
+ ---
34
+
10
35
  ## [1.22.1] – 2026-10-08
11
36
 
12
37
  ### Fixed
@@ -799,7 +824,9 @@ Starting with this release, GlassKit Elements version numbers are aligned with G
799
824
 
800
825
  ---
801
826
 
802
- [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.22.1...HEAD
827
+ [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.22.3...HEAD
828
+ [1.22.3]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.22.3
829
+ [1.22.2]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.22.2
803
830
  [1.22.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.22.1
804
831
  [1.22.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.22.0
805
832
  [1.21.2]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.21.2
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-44cc11?style=flat-square" alt="Vanilla JS"></a>
6
6
  <a href="#"><img src="https://img.shields.io/badge/components-36-7ec8e3?style=flat-square" alt="36 Components"></a>
7
7
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License"></a>
8
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.22.1-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.22.3-lightgrey?style=flat-square" alt="Changelog"></a>
9
9
  <a href="https://www.npmjs.com/package/@jungherz-de/glasskit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fglasskit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
10
10
  </p>
11
11
 
@@ -152,7 +152,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
152
152
 
153
153
  ### Form Elements
154
154
 
155
- All form components support `name`, `value`, `disabled` and participate in native `<form>` submission via `ElementInternals` — except `<glk-image-picker>`, which hands its result to `glk-change` instead: a data URL is no form value. Since 1.20.0 they also report their validity to the form (`required`, `pattern`, `min`, `type="email"` … stop a submit with the browser's own message), their `label` names the field for screen readers, and a click on it, `element.focus()` or a `<label for>` lands in the field.
155
+ All form components support `name`, `value`, `disabled` and participate in native `<form>` submission via `ElementInternals` — except `<glk-image-picker>`, which hands its result to `glk-change` instead: a data URL is no form value. Since 1.20.0 they also report their validity to the form (`required`, `pattern`, `min`, `type="email"` … stop a submit with the browser's own message), their `label` names the field for screen readers, and a click on it, `element.focus()` or a `<label for>` lands in the field. Since 1.22.3 a `<fieldset disabled>` around them disables them, as it does native controls.
156
156
 
157
157
  | Component | Description | Key Attributes |
158
158
  |---|---|---|
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: glasskit-elements
3
- description: GlassKit Elements is a vanilla-JS Web Components library (v1.22.1) wrapping GlassKit CSS v1.22.1. It provides 36 custom elements with the `glk-` prefix, Dark/Light mode with automatic theme sync, Shadow DOM encapsulation, and form-associated custom elements. Use this reference whenever generating HTML that uses `<glk-*>` tags to ensure correct attributes, slots, events, and composition.
3
+ description: GlassKit Elements is a vanilla-JS Web Components library (v1.22.3) wrapping GlassKit CSS v1.22.2. It provides 36 custom elements with the `glk-` prefix, Dark/Light mode with automatic theme sync, Shadow DOM encapsulation, and form-associated custom elements. Use this reference whenever generating HTML that uses `<glk-*>` tags to ensure correct attributes, slots, events, and composition.
4
4
  ---
5
5
 
6
6
  # GlassKit Elements – AI Component Reference
@@ -18,7 +18,7 @@ description: GlassKit Elements is a vanilla-JS Web Components library (v1.22.1)
18
18
  npm install @jungherz-de/glasskit-elements @jungherz-de/glasskit
19
19
  ```
20
20
 
21
- Peer dependency `@jungherz-de/glasskit >=1.22.1` is required — 1.22.1 lets date and time fields shrink in narrow columns, 1.22.0 brings the input affixes behind the `prefix` / `suffix` slots, 1.21.2 gives disabled controls their look, 1.21.1 keeps the controls' state on paper, 1.21.0 brings the density tokens behind `data-density`, 1.20.0 the rules the elements build on since then (the dialog overlay, hidden list slots, empty control labels, control inputs on top); 1.9.0 is the release that made the stylesheet splittable, which is what lets document-level branding reach the elements at all.
21
+ Peer dependency `@jungherz-de/glasskit >=1.22.2` is required — 1.22.2 hands a click on a disabled control in an input affix to the field, 1.22.1 lets date and time fields shrink in narrow columns, 1.22.0 brings the input affixes behind the `prefix` / `suffix` slots, 1.21.2 gives disabled controls their look, 1.21.1 keeps the controls' state on paper, 1.21.0 brings the density tokens behind `data-density`, 1.20.0 the rules the elements build on since then (the dialog overlay, hidden list slots, empty control labels, control inputs on top); 1.9.0 is the release that made the stylesheet splittable, which is what lets document-level branding reach the elements at all.
22
22
 
23
23
  ### Import (ES modules)
24
24
 
@@ -123,6 +123,7 @@ addEventListener('afterprint', () => {
123
123
  | Events | Custom `glk-*` events, all `bubbles: true, composed: true` |
124
124
  | Form participation | `GlkFormElement` uses `ElementInternals` (`static formAssociated = true`): value, reset and — since 1.20.0 — the inner field's validity |
125
125
  | Focus | Form elements with one field delegate focus (since 1.20.0): `element.focus()`, a click on the label, a `<label for>` land in the field |
126
+ | Disabled | Form elements follow their own `disabled` and a `<fieldset disabled>` around them, as native controls do (since 1.22.3); the `disabled` property reflects the attribute |
126
127
  | API style | Declarative HTML attributes + reflected JS properties |
127
128
 
128
129
  Custom properties (`--gl-*`) defined on `:root` or `<html>` pass through shadow boundaries by inheritance, so custom theming works with a single global stylesheet.
@@ -152,7 +153,7 @@ customElements.define('demo-counter', DemoCounter);
152
153
 
153
154
  Which import to take depends on how the elements are loaded, and mixing them is the one mistake to avoid: the `<script>` / ESM **bundle** carries its own copy of `GlkElement`, so a subclass built on `base.js` next to it is a different class — `instanceof` fails across the two, and the GlassKit stylesheet lives twice. Bundle loaded → take `GlassKitElements.GlkElement` or `import { GlkElement } from '@jungherz-de/glasskit-elements'`. Per-component files loaded → `base.js`, which the components import themselves. The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` external, so a bundler resolves it from `node_modules` and an import-map project maps it to the one copy it already loads for its own elements.
154
155
 
155
- Rules for a subclass: build only inside `this._wrapper` (it carries `data-theme`, and `data-density` while `<html>` has one); keep listeners in `setupEvents()` / `teardownEvents()`, never in `render()`, or a moved element loses them; use `emit()` instead of `dispatchEvent()` so the event bubbles and crosses the shadow boundary — `emit(name, detail, { cancelable: true })` lets a listener call `preventDefault()`, and `emit()` returns `false` then (since 1.19.0); return `true` from `static get displayInline()` for inline elements; set `static get observesLightDom()` to `true` and implement `projectLightDom()` when copying light-DOM children into the shadow tree. `GlkFormElement` adds `setFormValue()`, `setValidity()`, `resetValue()` / `restoreValue()` and `static formAssociated = true`.
156
+ Rules for a subclass: build only inside `this._wrapper` (it carries `data-theme`, and `data-density` while `<html>` has one); keep listeners in `setupEvents()` / `teardownEvents()`, never in `render()`, or a moved element loses them; use `emit()` instead of `dispatchEvent()` so the event bubbles and crosses the shadow boundary — `emit(name, detail, { cancelable: true })` lets a listener call `preventDefault()`, and `emit()` returns `false` then (since 1.19.0); return `true` from `static get displayInline()` for inline elements; set `static get observesLightDom()` to `true` and implement `projectLightDom()` when copying light-DOM children into the shadow tree. `GlkFormElement` adds `setFormValue()`, `setValidity()`, `resetValue()` / `restoreValue()`, a `disabled` property and `static formAssociated = true`. Override `applyDisabled(disabled)` to disable your native controls (since 1.22.3): it runs after the first render and whenever the element's own `disabled` or a `<fieldset disabled>` around it changes — no fieldset reaches into the shadow root by itself.
156
157
 
157
158
  ### Light-DOM children (since 1.12.0)
158
159
 
@@ -624,7 +625,7 @@ Text input with label + hint.
624
625
 
625
626
  Events: `glk-input` → `{ value }`, `glk-change` → `{ value }`. Native `input` / `change` also dispatched. Property: `.value`.
626
627
 
627
- **Slots `prefix` and `suffix` (since 1.22.0)** put an icon, a currency, a unit or a button inside the field's box, at its start and end (they swap sides under `dir="rtl"`). Each shows only while something is slotted; its width is measured and the field's padding grows by it, so the text keeps clear of a unit of any length. A click on slotted text or an icon focuses the field; a slotted button or link takes its own click. Their text is read as part of the field's description. Parts: `box`, `prefix`, `suffix`. Built on GlassKit's `.glass-input-wrap` (GlassKit 1.22.0).
628
+ **Slots `prefix` and `suffix` (since 1.22.0)** put an icon, a currency, a unit or a button inside the field's box, at its start and end (they swap sides under `dir="rtl"`). Each shows only while something is slotted; its width is measured and the field's padding grows by it, so the text keeps clear of a unit of any length. A click on slotted text, an icon or a disabled control goes through to the field; enabled controls (`<glk-button>`, `<glk-select>` … included — they are form-associated), links and elements with `tabindex` take their own (since 1.22.2 — before, every slotted element took the click). Slot the control itself: one wrapped in a slotted element needs `pointer-events: auto` from the page. A container locked with `pointer-events: none` does not hold a control; lock it with `inert`, or disable the control. Their text is read as part of the field's description. Parts: `box`, `prefix`, `suffix`. Built on GlassKit's `.glass-input-wrap` (GlassKit 1.22.0).
628
629
 
629
630
  ```html
630
631
  <glk-input label="Price" inputmode="decimal">
@@ -706,7 +707,7 @@ Slider input.
706
707
  | `name` | String | Form field name |
707
708
  | `disabled` | Boolean | Disabled state |
708
709
 
709
- Events: `glk-input`, `glk-change`. Property: `.value`.
710
+ Events: `glk-input`, `glk-change`. Properties: `.value`, `.disabled` (since 1.22.3).
710
711
 
711
712
  ---
712
713
 
@@ -726,7 +727,7 @@ Search input with leading icon.
726
727
  | `name` | String | Form field name |
727
728
  | `disabled` | Boolean | Disabled state |
728
729
 
729
- Events: `glk-input`, `glk-change`.
730
+ Events: `glk-input`, `glk-change`. Properties: `.value`, `.disabled` (since 1.22.3).
730
731
 
731
732
  ---
732
733
 
@@ -1086,8 +1087,9 @@ A small, exclusive choice as one control (since 1.15.0) — traffic light, morni
1086
1087
  | `overflow` | String | What happens when the options do not fit: `scroll` — one row that scrolls sideways, the chosen option kept in view; `wrap` — breaks into lines. Without it the row stays one line (since 1.17.0) |
1087
1088
  | `label` | String | `aria-label` of the group (no default — set it) |
1088
1089
  | `name` | String | Form field name |
1090
+ | `disabled` | Boolean | The whole row (since 1.22.3); a `<fieldset disabled>` around it disables it too |
1089
1091
 
1090
- Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`. Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.
1092
+ Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`, `disabled`. Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.
1091
1093
 
1092
1094
  With `overflow="scroll"` the chosen option is centred in the row after the first layout, on every value change and when the row's width changes — the row scrolls, never the page. Seven areas on a phone:
1093
1095
 
@@ -1202,8 +1204,9 @@ One month with a day to pick (since 1.16.0). The days are buttons in a `role="gr
1202
1204
  | `label` | String | `aria-label` of the day group |
1203
1205
  | `prev-label`, `next-label` | String | Names of the nav buttons ("Previous month" / "Next month") |
1204
1206
  | `name` | String | Form field name |
1207
+ | `disabled` | Boolean | Arrows and days (since 1.22.3); a `<fieldset disabled>` around it disables it too |
1205
1208
 
1206
- Events: `glk-change { value }` on a pick by the user; `glk-month { month }` when the user moves to another month. Properties: `month`, `value`, `marks`, `locale`, `label`. Parts: `calendar`, `head`, `title`, `grid`.
1209
+ Events: `glk-change { value }` on a pick by the user; `glk-month { month }` when the user moves to another month. Properties: `month`, `value`, `marks`, `locale`, `label`, `disabled`. Parts: `calendar`, `head`, `title`, `grid`.
1207
1210
 
1208
1211
  ---
1209
1212
 
@@ -238,6 +238,7 @@ class GlkElement extends HTMLElement {
238
238
  this._shadow.appendChild(this._wrapper);
239
239
 
240
240
  this.render();
241
+ if (this.constructor.formAssociated) this._syncDisabled();
241
242
  } else {
242
243
  // Back in the document. While it was out, the observer did not reach
243
244
  // it, so a theme or density switched in the meantime is caught up here
@@ -340,6 +341,28 @@ class GlkElement extends HTMLElement {
340
341
  */
341
342
  refresh() { this.projectLightDom(); }
342
343
 
344
+ /**
345
+ * Form-associated elements only. Such an element is disabled by its own
346
+ * `disabled` attribute or by a <fieldset disabled> around it: the browser
347
+ * counts both — :disabled, left out of the form data — and calls
348
+ * formDisabledCallback() on every change, also when the element moves into
349
+ * or out of such a fieldset. The native controls sit in the shadow root,
350
+ * out of every fieldset's reach, so the state is handed on to them through
351
+ * applyDisabled(). Until 1.22.2 only the element's own attribute reached
352
+ * them: inside a disabled fieldset every field stayed usable, and a submit
353
+ * button sent the form.
354
+ */
355
+ get _actuallyDisabled() { return this.matches(':disabled'); }
356
+
357
+ formDisabledCallback() {
358
+ if (this._initialized) this._syncDisabled();
359
+ }
360
+
361
+ _syncDisabled() { this.applyDisabled(this._actuallyDisabled); }
362
+
363
+ /** Form-associated subclasses override to disable their native controls. */
364
+ applyDisabled(disabled) {}
365
+
343
366
  // ── Utility Methods ──
344
367
 
345
368
  /**
@@ -417,6 +440,17 @@ class GlkFormElement extends GlkElement {
417
440
  if (this._initialized) this.syncValidity();
418
441
  }
419
442
 
443
+ // Reflects the attribute, as on a native control: a disabled fieldset
444
+ // around the element disables it without changing this.
445
+ get disabled() { return this.getBoolAttr('disabled'); }
446
+ set disabled(v) { this.setBoolAttr('disabled', v); }
447
+
448
+ _syncDisabled() {
449
+ super._syncDisabled();
450
+ // A disabled field is barred from validation; enabled again, it counts.
451
+ this.syncValidity();
452
+ }
453
+
420
454
  get form() { return this._internals.form; }
421
455
  get validationMessage() { return this._internals.validationMessage; }
422
456
  get validity() { return this._internals.validity; }
@@ -11,12 +11,13 @@ const FORWARDED_ARIA = ['aria-expanded', 'aria-haspopup', 'aria-pressed'];
11
11
 
12
12
  class GlkButton extends GlkElement {
13
13
  static get observedAttributes() {
14
- return ['variant', 'size', 'disabled', 'type', ...FORWARDED_ARIA];
14
+ return ['variant', 'size', 'type', ...FORWARDED_ARIA];
15
15
  }
16
16
 
17
17
  // The native button sits in the shadow root, outside every form. Form
18
18
  // association gives the element its form, so type="submit" and "reset"
19
- // can act on it (see _activateForm).
19
+ // can act on it (see _activateForm), and a disabled fieldset around it
20
+ // disables it (since 1.22.3; before, it still sent the form).
20
21
  static formAssociated = true;
21
22
 
22
23
  constructor() {
@@ -29,9 +30,6 @@ class GlkButton extends GlkElement {
29
30
  type: this.getAttribute('type') || 'button'
30
31
  });
31
32
 
32
- if (this.getBoolAttr('disabled')) {
33
- this._btn.disabled = true;
34
- }
35
33
  for (const attr of FORWARDED_ARIA) this._forward(attr);
36
34
 
37
35
  this._btn.appendChild(document.createElement('slot'));
@@ -40,7 +38,7 @@ class GlkButton extends GlkElement {
40
38
 
41
39
  setupEvents() {
42
40
  this._onClick = (e) => {
43
- if (this.getBoolAttr('disabled')) {
41
+ if (this._actuallyDisabled) {
44
42
  e.preventDefault();
45
43
  e.stopPropagation();
46
44
  return;
@@ -99,9 +97,6 @@ class GlkButton extends GlkElement {
99
97
  case 'size':
100
98
  this._btn.className = this._computeClasses().join(' ');
101
99
  break;
102
- case 'disabled':
103
- this._btn.disabled = this.getBoolAttr('disabled');
104
- break;
105
100
  case 'type':
106
101
  this._btn.setAttribute('type', this.getAttribute('type') || 'button');
107
102
  break;
@@ -110,6 +105,8 @@ class GlkButton extends GlkElement {
110
105
  }
111
106
  }
112
107
 
108
+ applyDisabled(disabled) { this._btn.disabled = disabled; }
109
+
113
110
  _forward(attr) {
114
111
  const value = this.getAttribute(attr);
115
112
  if (value === null) this._btn.removeAttribute(attr);
@@ -11,8 +11,9 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
11
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
- // (names of the nav buttons, English by default)
15
- // Properties: month, value, marks, locale, label
14
+ // (names of the nav buttons, English by default), disabled
15
+ // (since 1.22.3, as is a disabled fieldset around it)
16
+ // Properties: month, value, marks, locale, label, disabled
16
17
  // Form: associated — a surrounding <form> receives name=value, reset
17
18
  // restores the initial value, like <glk-segmented>
18
19
  // Events: glk-change { value } — on a pick by the user
@@ -104,9 +105,16 @@ class GlkCalendar extends GlkFormElement {
104
105
  day.append(num, dots);
105
106
  this._grid.appendChild(day);
106
107
  }
108
+ this.applyDisabled(this._actuallyDisabled);
107
109
  this._syncValue();
108
110
  }
109
111
 
112
+ // Disabled as a whole: the arrows and every day. The days of another month
113
+ // are built again, so _build() hands the state on to them too.
114
+ applyDisabled(disabled) {
115
+ for (const button of [this._prev, this._next, ...this._days()]) button.disabled = disabled;
116
+ }
117
+
110
118
  _days() { return [...this._grid.querySelectorAll('button[data-value]')]; }
111
119
  _dayFor(iso) { return this._grid.querySelector(`button[data-value="${iso}"]`); }
112
120
 
@@ -5,7 +5,7 @@ const CHECKMARK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor
5
5
 
6
6
  class GlkCheckbox extends GlkFormElement {
7
7
  static get observedAttributes() {
8
- return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
8
+ return ['checked', 'label', 'name', 'value', 'required'];
9
9
  }
10
10
 
11
11
  static get hostStyles() { return checkControlSheet; }
@@ -31,7 +31,6 @@ class GlkCheckbox extends GlkFormElement {
31
31
  label.appendChild(this._labelEl);
32
32
 
33
33
  if (this.getBoolAttr('checked')) this._input.checked = true;
34
- if (this.getBoolAttr('disabled')) this._input.disabled = true;
35
34
  if (this.getBoolAttr('required')) this._input.required = true;
36
35
 
37
36
  this._defaultChecked = this.getBoolAttr('checked');
@@ -65,9 +64,6 @@ class GlkCheckbox extends GlkFormElement {
65
64
  this._input.checked = this.getBoolAttr('checked');
66
65
  this._syncFormValue();
67
66
  break;
68
- case 'disabled':
69
- this._input.disabled = this.getBoolAttr('disabled');
70
- break;
71
67
  case 'label':
72
68
  this._labelEl.textContent = this.getAttribute('label') || '';
73
69
  break;
@@ -80,6 +76,8 @@ class GlkCheckbox extends GlkFormElement {
80
76
  }
81
77
  }
82
78
 
79
+ applyDisabled(disabled) { this._input.disabled = disabled; }
80
+
83
81
  get _validityField() { return this._input; }
84
82
 
85
83
  _syncFormValue() {
@@ -101,9 +99,6 @@ class GlkCheckbox extends GlkFormElement {
101
99
  if (this._input) this._syncFormValue();
102
100
  }
103
101
 
104
- get disabled() { return this.getBoolAttr('disabled'); }
105
- set disabled(v) { this.setBoolAttr('disabled', v); }
106
-
107
102
  get name() { return this.getAttribute('name'); }
108
103
  set name(v) { this.setAttribute('name', v); }
109
104
 
@@ -10,19 +10,30 @@ const FORWARDED = ['min', 'max', 'step', 'minlength', 'maxlength', 'pattern', 'a
10
10
  // Prefix and suffix (since 1.22.0): two slots inside the field's box, on
11
11
  // GlassKit's .glass-input-wrap. An affix shows only while something is
12
12
  // slotted into it, and its width is measured, so the text keeps clear of a
13
- // unit of any length. GlassKit lets clicks through an affix to the field;
14
- // slotted elements take them here instead — a button or a link inside
15
- // works — and a click on slotted text or an icon still lands in the field,
16
- // because the shadow root delegates focus to it.
13
+ // unit of any length. As in GlassKit, a click on text, an icon or a disabled
14
+ // control goes through to the field, and what can be used takes its own: an
15
+ // enabled control (<glk-button>, <glk-select> … are form-associated, so they
16
+ // count), a link, an element with tabindex. GlassKit's own rule cannot reach
17
+ // slotted elements, hence this one. ::slotted() sees the slotted element,
18
+ // not what is inside it: a button wrapped in a slotted <span> needs
19
+ // pointer-events: auto from the page. Until 1.22.1 every slotted element took
20
+ // the pointer — a unit caught the click meant for the field, a disabled
21
+ // button took it and moved the focus into the field, read-only and locked
22
+ // or not. Simple selectors only inside ::slotted(), so that no browser drops
23
+ // the rule.
17
24
  const affixSheet = new CSSStyleSheet();
18
25
  affixSheet.replaceSync(`
19
- .glass-input-wrap__prefix ::slotted(*),
20
- .glass-input-wrap__suffix ::slotted(*) { pointer-events: auto; }
26
+ .glass-input-wrap__prefix ::slotted(:enabled),
27
+ .glass-input-wrap__prefix ::slotted(a[href]),
28
+ .glass-input-wrap__prefix ::slotted([tabindex]:not(:disabled)),
29
+ .glass-input-wrap__suffix ::slotted(:enabled),
30
+ .glass-input-wrap__suffix ::slotted(a[href]),
31
+ .glass-input-wrap__suffix ::slotted([tabindex]:not(:disabled)) { pointer-events: auto; }
21
32
  `);
22
33
 
23
34
  class GlkInput extends GlkFormElement {
24
35
  static get observedAttributes() {
25
- return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'readonly', 'name', 'value', 'required', ...FORWARDED];
36
+ return ['label', 'type', 'placeholder', 'error', 'hint', 'readonly', 'name', 'value', 'required', ...FORWARDED];
26
37
  }
27
38
 
28
39
  static get hostStyles() { return affixSheet; }
@@ -53,7 +64,6 @@ class GlkInput extends GlkFormElement {
53
64
  this._pendingValue = undefined;
54
65
  if (value) this._input.value = value;
55
66
 
56
- if (this.getBoolAttr('disabled')) this._input.disabled = true;
57
67
  if (this.getBoolAttr('readonly')) this._input.readOnly = true;
58
68
  if (this.getBoolAttr('required')) this._input.required = true;
59
69
  for (const attr of FORWARDED) this._forward(attr);
@@ -143,9 +153,6 @@ class GlkInput extends GlkFormElement {
143
153
  }
144
154
  this._applyDescription();
145
155
  break;
146
- case 'disabled':
147
- this._input.disabled = this.getBoolAttr('disabled');
148
- break;
149
156
  case 'readonly':
150
157
  this._input.readOnly = this.getBoolAttr('readonly');
151
158
  break;
@@ -164,6 +171,8 @@ class GlkInput extends GlkFormElement {
164
171
  }
165
172
  }
166
173
 
174
+ applyDisabled(disabled) { this._input.disabled = disabled; }
175
+
167
176
  /** An affix shows, and makes room in the field, only while something is slotted. */
168
177
  _applyAffixes() {
169
178
  for (const affix of [this._prefix, this._suffix]) {
@@ -229,9 +238,6 @@ class GlkInput extends GlkFormElement {
229
238
  this._syncFormValue();
230
239
  }
231
240
 
232
- get disabled() { return this.getBoolAttr('disabled'); }
233
- set disabled(v) { this.setBoolAttr('disabled', v); }
234
-
235
241
  get readOnly() { return this.getBoolAttr('readonly'); }
236
242
  set readOnly(v) { this.setBoolAttr('readonly', v); }
237
243
 
@@ -20,7 +20,7 @@ function ownerForm(el) {
20
20
 
21
21
  /** Only the selected radio is a tab stop; arrow keys move within the group. */
22
22
  function syncGroupTabIndex(group) {
23
- const enabled = group.filter(el => !el.disabled);
23
+ const enabled = group.filter(el => !el._actuallyDisabled);
24
24
  if (!enabled.length) return;
25
25
  const focusable = enabled.find(el => el.checked) || enabled[0];
26
26
  for (const el of group) {
@@ -30,7 +30,7 @@ function syncGroupTabIndex(group) {
30
30
 
31
31
  class GlkRadio extends GlkFormElement {
32
32
  static get observedAttributes() {
33
- return ['checked', 'disabled', 'label', 'name', 'value', 'required'];
33
+ return ['checked', 'label', 'name', 'value', 'required'];
34
34
  }
35
35
 
36
36
  static get hostStyles() { return checkControlSheet; }
@@ -60,7 +60,6 @@ class GlkRadio extends GlkFormElement {
60
60
  label.appendChild(this._labelEl);
61
61
 
62
62
  if (this.getBoolAttr('checked')) this._input.checked = true;
63
- if (this.getBoolAttr('disabled')) this._input.disabled = true;
64
63
 
65
64
  this._defaultChecked = this.getBoolAttr('checked');
66
65
  this._wrapper.appendChild(label);
@@ -78,7 +77,7 @@ class GlkRadio extends GlkFormElement {
78
77
  this._onKeyDown = (e) => {
79
78
  const dir = ARROW_KEYS[e.key];
80
79
  if (!dir || e.ctrlKey || e.metaKey || e.altKey) return;
81
- const group = this._group().filter(el => !el.disabled);
80
+ const group = this._group().filter(el => !el._actuallyDisabled);
82
81
  if (group.length < 2) return;
83
82
  e.preventDefault();
84
83
  const next = group[(group.indexOf(this) + dir + group.length) % group.length];
@@ -160,10 +159,6 @@ class GlkRadio extends GlkFormElement {
160
159
  this._syncFormValue();
161
160
  syncGroupTabIndex(this._group());
162
161
  break;
163
- case 'disabled':
164
- this._input.disabled = this.getBoolAttr('disabled');
165
- syncGroupTabIndex(this._group());
166
- break;
167
162
  case 'label':
168
163
  this._labelEl.textContent = this.getAttribute('label') || '';
169
164
  break;
@@ -178,6 +173,11 @@ class GlkRadio extends GlkFormElement {
178
173
  }
179
174
  }
180
175
 
176
+ applyDisabled(disabled) {
177
+ this._input.disabled = disabled;
178
+ syncGroupTabIndex(this._group());
179
+ }
180
+
181
181
  _syncFormValue() {
182
182
  const val = this.getAttribute('value') || '';
183
183
  this.setFormValue(this._input.checked ? val : null);
@@ -202,9 +202,6 @@ class GlkRadio extends GlkFormElement {
202
202
  syncGroupTabIndex(this._group());
203
203
  }
204
204
 
205
- get disabled() { return this.getBoolAttr('disabled'); }
206
- set disabled(v) { this.setBoolAttr('disabled', v); }
207
-
208
205
  get name() { return this.getAttribute('name'); }
209
206
  set name(v) { this.setAttribute('name', v); }
210
207
 
@@ -3,7 +3,7 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
4
  class GlkRange extends GlkFormElement {
5
5
  static get observedAttributes() {
6
- return ['label', 'min', 'max', 'value', 'step', 'name', 'disabled'];
6
+ return ['label', 'min', 'max', 'value', 'step', 'name'];
7
7
  }
8
8
 
9
9
  render() {
@@ -34,8 +34,6 @@ class GlkRange extends GlkFormElement {
34
34
  const name = this.getAttribute('name');
35
35
  if (name) this._input.setAttribute('name', name);
36
36
 
37
- if (this.getBoolAttr('disabled')) this._input.disabled = true;
38
-
39
37
  // The reset value is the attribute's; a value set as a property before
40
38
  // render() has waited for it and comes on top.
41
39
  this._defaultValue = this._input.value;
@@ -87,12 +85,11 @@ class GlkRange extends GlkFormElement {
87
85
  case 'name':
88
86
  this._input.setAttribute('name', this.getAttribute('name') || '');
89
87
  break;
90
- case 'disabled':
91
- this._input.disabled = this.getBoolAttr('disabled');
92
- break;
93
88
  }
94
89
  }
95
90
 
91
+ applyDisabled(disabled) { this._input.disabled = disabled; }
92
+
96
93
  _updateValueDisplay() {
97
94
  this._valueEl.textContent = `${this._input.value}%`;
98
95
  }
@@ -5,7 +5,7 @@ const SEARCH_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
5
5
 
6
6
  class GlkSearch extends GlkFormElement {
7
7
  static get observedAttributes() {
8
- return ['placeholder', 'name', 'value', 'disabled', 'label'];
8
+ return ['placeholder', 'name', 'value', 'label'];
9
9
  }
10
10
 
11
11
  render() {
@@ -33,8 +33,6 @@ class GlkSearch extends GlkFormElement {
33
33
  this._pendingValue = undefined;
34
34
  if (value) this._input.value = value;
35
35
 
36
- if (this.getBoolAttr('disabled')) this._input.disabled = true;
37
-
38
36
  container.appendChild(icon);
39
37
  container.appendChild(this._input);
40
38
 
@@ -68,15 +66,14 @@ class GlkSearch extends GlkFormElement {
68
66
  this._input.value = this.getAttribute('value') || '';
69
67
  this._syncFormValue();
70
68
  break;
71
- case 'disabled':
72
- this._input.disabled = this.getBoolAttr('disabled');
73
- break;
74
69
  case 'label':
75
70
  this._applyLabel();
76
71
  break;
77
72
  }
78
73
  }
79
74
 
75
+ applyDisabled(disabled) { this._input.disabled = disabled; }
76
+
80
77
  _applyLabel() {
81
78
  const label = this.getAttribute('label');
82
79
  if (label) this._input.setAttribute('aria-label', label);
@@ -8,8 +8,11 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
8
8
  // Attributes: options (JSON [{value,label,tone?,disabled?}]), value, full,
9
9
  // overflow ("scroll" | "wrap": what happens when the options
10
10
  // do not fit — without it the row stays one line and runs past
11
- // the edge), label (aria-label of the group), name (form field)
12
- // Properties: value, options (array or JSON text), full, overflow, label
11
+ // the edge), label (aria-label of the group), name (form field),
12
+ // disabled (the whole row; since 1.22.3, as is a disabled
13
+ // fieldset around it)
14
+ // Properties: value, options (array or JSON text), full, overflow, label,
15
+ // disabled
13
16
  // Event: glk-change { value } — only on a change made by the user
14
17
  // Part: group
15
18
  //
@@ -62,13 +65,21 @@ class GlkSegmented extends GlkFormElement {
62
65
  button.classList.add(`glass-segmented__item--${option.tone}`);
63
66
  button.appendChild(this.createElement('span', ['glass-segmented__dot']));
64
67
  }
65
- if (option.disabled) button.disabled = true;
66
68
  button.appendChild(document.createTextNode(option.label ?? value));
67
69
  this._group.appendChild(button);
68
70
  }
71
+ this.applyDisabled(this._actuallyDisabled);
69
72
  this._syncValue();
70
73
  }
71
74
 
75
+ /** The whole row is disabled, or an option by its own `disabled`. */
76
+ applyDisabled(disabled) {
77
+ const options = this.options;
78
+ this._group.querySelectorAll('button[data-value]').forEach((button, i) => {
79
+ button.disabled = disabled || Boolean(options[i]?.disabled);
80
+ });
81
+ }
82
+
72
83
  _syncValue() {
73
84
  const value = this.value;
74
85
  for (const button of this._group.querySelectorAll('button[data-value]')) {
@@ -13,7 +13,7 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
13
13
 
14
14
  class GlkSelect extends GlkFormElement {
15
15
  static get observedAttributes() {
16
- return ['label', 'disabled', 'name', 'value', 'required'];
16
+ return ['label', 'name', 'value', 'required'];
17
17
  }
18
18
 
19
19
  static get observesLightDom() { return true; }
@@ -32,7 +32,6 @@ class GlkSelect extends GlkFormElement {
32
32
  const name = this.getAttribute('name');
33
33
  if (name) this._select.setAttribute('name', name);
34
34
 
35
- if (this.getBoolAttr('disabled')) this._select.disabled = true;
36
35
  if (this.getBoolAttr('required')) this._select.required = true;
37
36
 
38
37
  group.appendChild(this._labelEl);
@@ -106,9 +105,6 @@ class GlkSelect extends GlkFormElement {
106
105
  case 'label':
107
106
  this._labelEl.textContent = this.getAttribute('label') || '';
108
107
  break;
109
- case 'disabled':
110
- this._select.disabled = this.getBoolAttr('disabled');
111
- break;
112
108
  case 'name':
113
109
  this._select.setAttribute('name', this.getAttribute('name') || '');
114
110
  break;
@@ -123,6 +119,8 @@ class GlkSelect extends GlkFormElement {
123
119
  }
124
120
  }
125
121
 
122
+ applyDisabled(disabled) { this._select.disabled = disabled; }
123
+
126
124
  get _validityField() { return this._select; }
127
125
 
128
126
  _syncFormValue() {
@@ -148,9 +146,6 @@ class GlkSelect extends GlkFormElement {
148
146
  this._applyValue(this._wanted);
149
147
  this._syncFormValue();
150
148
  }
151
-
152
- get disabled() { return this.getBoolAttr('disabled'); }
153
- set disabled(v) { this.setBoolAttr('disabled', v); }
154
149
  }
155
150
 
156
151
  customElements.define('glk-select', GlkSelect);