@jungherz-de/glasskit-elements 1.21.1 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.22.0] – 2026-10-07
11
+
12
+ Versions realign with GlassKit at 1.22.0, which this release needs (peer dependency `>=1.22.0`) for the input affixes.
13
+
14
+ ### Added
15
+
16
+ - **`<glk-input>` has `prefix` and `suffix` slots.** It had no slot at all, so a currency, a unit, an icon or a button inside the field's box had to be laid over it from outside, with the field's padding changed through the shadow root to keep the text clear. Now `<span slot="suffix">EUR</span>` sits inside the box at the end, a `slot="prefix"` icon at the start — right to left they swap sides —, both centred on the field. An affix shows only while something is slotted into it, and its width is measured, so the text keeps clear of a unit of any length and the room is there from the first frame, without sliding. A click on slotted text or an icon focuses the field; a slotted button or link takes its own click. Their text is read with the field as its description. New parts: `box`, `prefix`, `suffix`. Built on GlassKit 1.22.0's `.glass-input-wrap`. Without slotted content a field renders exactly as before — the docs, showcase and landing pages are pixel-identical in Chromium and WebKit. Measured: "EUR" 16 px from the end with the field's padding at 16 + 29 + 8 px, "https://" as prefix 48 px with the text after it, an 18 px icon giving 42 px; an affix added later shows and makes room, removed, the field is back to 16 px. ([#12](https://github.com/JUNGHERZ/GlassKit-Elements/issues/12))
17
+ - **`readonly` on `<glk-input>` and `<glk-textarea>`,** with a `readOnly` property. The attribute did not reach the field: a record shown before an edit mode was switched on stayed editable, and typing emitted `glk-input` and `glk-change`. The inner field is read-only now — focusable, selectable and submitted with the form, not editable, and barred from validation like a native read-only field. Measured in Chromium and WebKit: typing into `<glk-input readonly value="Ada">` leaves "Ada", the field takes the focus, the form submits "Ada"; `readOnly = false` makes it editable again. ([#11](https://github.com/JUNGHERZ/GlassKit-Elements/issues/11))
18
+
19
+ ### Fixed
20
+
21
+ - **A value set as a property survives, whatever the moment.** Frameworks bind values as properties right after creating an element — hybrids whenever the name is a property, lit's `.value`, Vue —, before it is connected and before `<glk-select>` has copied its options in. Measured across the elements, before this release: setting `value` on `<glk-select>`, `<glk-input>`, `<glk-textarea>`, `<glk-search>` or `<glk-range>`, or `checked` on `<glk-checkbox>`, `<glk-toggle>` or `<glk-radio>`, before connecting threw a `TypeError`; a `<glk-select>` given its value after connecting but before the options were copied showed its first option while the app held the other value, and saving kept the value the user had not seen. Now every such setter keeps what it cannot apply yet and `render()` takes it up, the getters read it back meanwhile, and `<glk-select>` keeps a value naming no option yet as the wanted value — selected as soon as an option carries it, until the user picks another. That also makes good the documented promise that a `value` attribute whose option arrives later is selected then: a surviving default selection used to win. A form reset returns `<glk-select>` to its `value` attribute instead of the first option. ([#13](https://github.com/JUNGHERZ/GlassKit-Elements/issues/13))
22
+ - **Properties set before the element was defined are no longer lost.** A property a framework sets while the bundle is still loading lands on the element itself and hides the class's setter — `<glk-list header>`, `<glk-progress value>`, `<glk-image-picker label>` and the form values above showed nothing of it. On first connect such a value is taken off the instance and set again through the setter. Checked for all nineteen settable properties, set before connecting, before the definition and after connecting: each one shows and reads back. ([#13](https://github.com/JUNGHERZ/GlassKit-Elements/issues/13))
23
+
24
+ ### Documentation
25
+
26
+ - Docs (English and German), README and SKILL.md describe the slots, `readonly`, the wanted value of `<glk-select>` and that properties set early are kept. The prose that 1.20.0 to 1.21.2 added to SKILL.md and README is on one line per paragraph now, as the rest of the new text.
27
+
28
+ ---
29
+
30
+ ## [1.21.2] – 2026-10-05
31
+
32
+ ### Fixed
33
+
34
+ - **Disabled elements look disabled.** Rebuilt against GlassKit 1.21.2, which adds the disabled state its controls were missing. `<glk-button disabled>` rendered exactly like an enabled button, with the pointer cursor and the hover lift; so did `<glk-pill>` and `<glk-tab-accessory>`, and `<glk-toggle>`, `<glk-checkbox>`, `<glk-radio>`, `<glk-textarea>` and `<glk-range>` showed no difference at all — `<glk-select>` was dimmed by Chrome's own stylesheet only. Now buttons, pill, accessory and the check controls dim to 0.45 with the not-allowed cursor and no hover effect, the fields to 0.4 like `<glk-input>`. Measured in Chromium and WebKit, opacity of the inner control with `disabled` on the element: 1 → 0.45 for button, pill, accessory, toggle, checkbox and radio, 1 (Chrome: 0.7 for the select) → 0.4 for select, textarea and range. The rules are part of GlassKit's component sheet, so they reach the shadow roots; the bundles carry that sheet and needed the rebuild. The disabled toggle and button on the showcase and the docs now look disabled. The peer dependency moves to `>=1.21.2`. ([GlassKit #7](https://github.com/JUNGHERZ/GlassKit/issues/7))
35
+
36
+ ---
37
+
10
38
  ## [1.21.1] – 2026-10-05
11
39
 
12
40
  ### Fixed
@@ -763,7 +791,9 @@ Starting with this release, GlassKit Elements version numbers are aligned with G
763
791
 
764
792
  ---
765
793
 
766
- [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.21.1...HEAD
794
+ [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.22.0...HEAD
795
+ [1.22.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.22.0
796
+ [1.21.2]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.21.2
767
797
  [1.21.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.21.1
768
798
  [1.21.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.21.0
769
799
  [1.20.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.20.0
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.21.1-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.22.0-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
 
@@ -66,7 +66,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
66
66
 
67
67
  ```html
68
68
  <!-- 1. GlassKit CSS -->
69
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.21/glasskit.min.css">
69
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.22/glasskit.min.css">
70
70
 
71
71
  <!-- 2. GlassKit Elements -->
72
72
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
@@ -104,7 +104,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
104
104
  <!DOCTYPE html>
105
105
  <html data-theme="dark">
106
106
  <head>
107
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.21/glasskit.min.css">
107
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.22/glasskit.min.css">
108
108
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
109
109
  </head>
110
110
  <body>
@@ -156,8 +156,8 @@ All form components support `name`, `value`, `disabled` and participate in nativ
156
156
 
157
157
  | Component | Description | Key Attributes |
158
158
  |---|---|---|
159
- | `<glk-input>` | Text input with label & hint | `label`, `type`, `placeholder`, `hint`, `error`, `required`, `min`, `max`, `step`, `maxlength`, `pattern`, `autocomplete`, `inputmode` |
160
- | `<glk-textarea>` | Multi-line text input | `label`, `rows`, `placeholder`, `required` |
159
+ | `<glk-input>` | Text input with label & hint; slots `prefix` / `suffix` for an icon, unit or button in the field | `label`, `type`, `placeholder`, `hint`, `error`, `required`, `readonly`, `min`, `max`, `step`, `maxlength`, `pattern`, `autocomplete`, `inputmode` |
160
+ | `<glk-textarea>` | Multi-line text input | `label`, `rows`, `placeholder`, `required`, `readonly` |
161
161
  | `<glk-select>` | Dropdown select | `label`, `required` (children: `<option>`) |
162
162
  | `<glk-search>` | Search input with icon | `placeholder`, `label` (accessible name) |
163
163
  | `<glk-toggle>` | Switch toggle (`role="switch"` on its native checkbox) | `label`, `checked`, `disabled`, `required` |
@@ -267,13 +267,7 @@ inherits them — no JavaScript, no per-element setup:
267
267
  Token defaults are placed on the document once, inside `@layer glasskit-defaults`, so
268
268
  pages that never load `glasskit.css` still work and your brand file always wins.
269
269
 
270
- Since 1.20.0 that layer carries the `--gl-*` tokens only — GlassKit's `color-scheme` stays
271
- off the page. Each element sets it on its own theme wrapper, so a select's list or a date
272
- picker inside matches the element while the page keeps its scheme. (Before, importing the
273
- elements turned the whole page to `color-scheme: dark`: unstyled text white, links light
274
- blue, native controls dark.) The token names are still global, so a component of yours
275
- that reads `var(--gl-…, fallback)` gets GlassKit's value. A page that declares the tokens
276
- itself, or wants them on part of the page only, switches the defaults off:
270
+ Since 1.20.0 that layer carries the `--gl-*` tokens only — GlassKit's `color-scheme` stays off the page. Each element sets it on its own theme wrapper, so a select's list or a date picker inside matches the element while the page keeps its scheme. (Before, importing the elements turned the whole page to `color-scheme: dark`: unstyled text white, links light blue, native controls dark.) The token names are still global, so a component of yours that reads `var(--gl-…, fallback)` gets GlassKit's value. A page that declares the tokens itself, or wants them on part of the page only, switches the defaults off:
277
271
 
278
272
  ```html
279
273
  <html data-glk-defaults="off"> <!-- watched: may be set or removed later -->
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.21.1) wrapping GlassKit CSS v1.21.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.0) wrapping GlassKit CSS v1.22.0. 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.21.1)
18
18
  npm install @jungherz-de/glasskit-elements @jungherz-de/glasskit
19
19
  ```
20
20
 
21
- Peer dependency `@jungherz-de/glasskit >=1.21.1` is required — 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.0` is required — 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
 
@@ -36,11 +36,7 @@ import '@jungherz-de/glasskit-elements/components/glk-button.js';
36
36
  import { GlkButton } from '@jungherz-de/glasskit-elements/components/glk-button.js';
37
37
  ```
38
38
 
39
- Every element has its own entry at `components/glk-{name}.js`; they import `./base.js` and leave `@jungherz-de/glasskit/glasskit-styles.js` external (bundler or import map resolves it). Importing one pulls that
40
- file plus `base.js` (~16 KB, shared by all of them) and GlassKit's `glasskit-styles.js`
41
- (~70 KB, resolved once by your project) rather than the 221 KB full bundle; several imports
42
- share both. Each module registers its
43
- custom element as a side effect, so a bare `import '…'` is enough.
39
+ Every element has its own entry at `components/glk-{name}.js`; they import `./base.js` and leave `@jungherz-de/glasskit/glasskit-styles.js` external (bundler or import map resolves it). Importing one pulls that file plus `base.js` (~16 KB, shared by all of them) and GlassKit's `glasskit-styles.js` (~70 KB, resolved once by your project) rather than the 221 KB full bundle; several imports share both. Each module registers its custom element as a side effect, so a bare `import '…'` is enough.
44
40
 
45
41
  ### CDN
46
42
 
@@ -214,22 +210,15 @@ this module puts them on the document once, wrapped in `@layer glasskit-defaults
214
210
  If your app assigns `document.adoptedStyleSheets = [...]` wholesale at some later point,
215
211
  re-append the existing entries rather than replacing them, or the defaults are lost.
216
212
 
217
- **Only the tokens, not `color-scheme` (since 1.20.0).** GlassKit's token blocks also set
218
- `color-scheme`; on the document that switched the whole page to dark — unstyled text white,
219
- links light blue, native controls dark — on pages that never asked for GlassKit's theme. The
220
- layer now carries the `--gl-*` tokens only, and every element sets `color-scheme` on its own
221
- theme wrapper, so native parts inside it (a select's list, a date picker) match the element.
213
+ **Only the tokens, not `color-scheme` (since 1.20.0).** GlassKit's token blocks also set `color-scheme`; on the document that switched the whole page to dark — unstyled text white, links light blue, native controls dark — on pages that never asked for GlassKit's theme. The layer now carries the `--gl-*` tokens only, and every element sets `color-scheme` on its own theme wrapper, so native parts inside it (a select's list, a date picker) match the element.
222
214
 
223
- **Opting out.** The token names are global, so a component of the page that reads
224
- `var(--gl-…, fallback)` sees GlassKit's values. A page that declares the tokens itself, or
225
- wants them in part of the page only, switches the defaults off — the attribute is watched:
215
+ **Opting out.** The token names are global, so a component of the page that reads `var(--gl-…, fallback)` sees GlassKit's values. A page that declares the tokens itself, or wants them in part of the page only, switches the defaults off — the attribute is watched:
226
216
 
227
217
  ```html
228
218
  <html data-glk-defaults="off">
229
219
  ```
230
220
 
231
- The elements then inherit the tokens from `glasskit.css` or from a block of the page's own,
232
- for instance on a container around them.
221
+ The elements then inherit the tokens from `glasskit.css` or from a block of the page's own, for instance on a container around them.
233
222
 
234
223
  ### Passing icons (fixed in 1.10.0)
235
224
 
@@ -323,8 +312,7 @@ Default slot: icon SVG (24×24).
323
312
 
324
313
  Events: `glk-click` when clicked.
325
314
 
326
- `aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the
327
- native button inside (since 1.20.0) — `<glk-popover>` sets `aria-expanded` on its trigger.
315
+ `aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the native button inside (since 1.20.0) — `<glk-popover>` sets `aria-expanded` on its trigger.
328
316
 
329
317
  ---
330
318
 
@@ -574,20 +562,11 @@ Full-width glass button (56 px). Three variants, four sizes.
574
562
 
575
563
  Events: `glk-click` (suppressed when disabled).
576
564
 
577
- **Submitting (since 1.20.0).** The element is form-associated: `type="submit"` submits the
578
- form it sits in and `type="reset"` resets it. Before 1.20.0 both did nothing — the native
579
- button lives in the shadow root, outside every form. It acts once the click has finished
580
- bubbling, so `preventDefault()` on the click stops it, as on a native button; invalid fields
581
- stop the submit with the browser's message; the submit event's `submitter` is the element;
582
- `.form` is its form; `element.click()` acts like a click. No implicit submission: Enter in a
583
- `<glk-input>` does not submit the form.
565
+ **Submitting (since 1.20.0).** The element is form-associated: `type="submit"` submits the form it sits in and `type="reset"` resets it. Before 1.20.0 both did nothing — the native button lives in the shadow root, outside every form. It acts once the click has finished bubbling, so `preventDefault()` on the click stops it, as on a native button; invalid fields stop the submit with the browser's message; the submit event's `submitter` is the element; `.form` is its form; `element.click()` acts like a click. No implicit submission: Enter in a `<glk-input>` does not submit the form.
584
566
 
585
- **Upgrading from ≤ 1.19:** code that set `type="submit"` and did the work in a `glk-click`
586
- handler now gets a real submit as well — handle the form's `submit` event (with
587
- `preventDefault()` in an SPA) or drop `type="submit"`, or the browser reloads the page.
567
+ **Upgrading from ≤ 1.19:** code that set `type="submit"` and did the work in a `glk-click` handler now gets a real submit as well — handle the form's `submit` event (with `preventDefault()` in an SPA) or drop `type="submit"`, or the browser reloads the page.
588
568
 
589
- `aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the
590
- native button inside, which is what screen readers read (since 1.20.0).
569
+ `aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the native button inside, which is what screen readers read (since 1.20.0).
591
570
 
592
571
  Default slot: button label (text or SVG + text).
593
572
 
@@ -613,9 +592,7 @@ Form-associated checkbox with glass styling.
613
592
 
614
593
  Events: `glk-change` → `{ checked }`; also dispatches native `change`. Property: `.checked`.
615
594
 
616
- `element.click()` and a `<label>` around the element or naming it tick it, as they do a
617
- native checkbox (since 1.20.0). A press on the element's empty width (it is a block, the
618
- control narrower) still does nothing.
595
+ `element.click()` and a `<label>` around the element or naming it tick it, as they do a native checkbox (since 1.20.0). A press on the element's empty width (it is a block, the control narrower) still does nothing.
619
596
 
620
597
  ---
621
598
 
@@ -638,6 +615,7 @@ Text input with label + hint.
638
615
  | `error` | Boolean | Error styling (red border + red hint) and `aria-invalid` on the field — it does not block the form |
639
616
  | `disabled` | Boolean | Disabled state |
640
617
  | `required` | Boolean | Required field — the form does not submit while it is empty (since 1.20.0) |
618
+ | `readonly` | Boolean | Read-only: focusable, selectable and submitted, not editable, barred from validation like a native field; property `.readOnly` (since 1.22.0) |
641
619
  | `name` | String | Form field name |
642
620
  | `value` | String | Current value |
643
621
  | `min`, `max`, `step` | String | Passed to the inner field — range and step of the picker for dates, times and numbers (since 1.17.0) |
@@ -646,18 +624,20 @@ Text input with label + hint.
646
624
 
647
625
  Events: `glk-input` → `{ value }`, `glk-change` → `{ value }`. Native `input` / `change` also dispatched. Property: `.value`.
648
626
 
649
- **Validity (since 1.20.0).** The element reports its inner field's validity to the form:
650
- `required`, `pattern`, `min`/`max`, `minlength`, `type="email"` … make `form.checkValidity()`
651
- false and stop a submit at the first invalid field with the browser's own message;
652
- `reportValidity()` focuses the field. `element.validity`, `.validationMessage`,
653
- `.willValidate`, `.checkValidity()` and `.reportValidity()` answer on the element. Before
654
- 1.20.0 none of this reached the form. `error` stays a marker: a server's verdict arrives that
655
- way, and it must not block the next attempt.
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).
656
628
 
657
- **Label and focus (since 1.20.0).** The `label` is tied to the field: it names it for screen
658
- readers, and a click on it focuses it — as do `element.focus()` and a `<label for>` naming the
659
- element. An outer `<label for>` focuses, but cannot name, the field inside the shadow root;
660
- use `label` for the name.
629
+ ```html
630
+ <glk-input label="Price" inputmode="decimal">
631
+ <span slot="suffix">EUR</span>
632
+ </glk-input>
633
+ <glk-input label="Search" type="search">
634
+ <svg slot="prefix" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
635
+ </glk-input>
636
+ ```
637
+
638
+ **Validity (since 1.20.0).** The element reports its inner field's validity to the form: `required`, `pattern`, `min`/`max`, `minlength`, `type="email"` … make `form.checkValidity()` false and stop a submit at the first invalid field with the browser's own message; `reportValidity()` focuses the field. `element.validity`, `.validationMessage`, `.willValidate`, `.checkValidity()` and `.reportValidity()` answer on the element. Before 1.20.0 none of this reached the form. `error` stays a marker: a server's verdict arrives that way, and it must not block the next attempt.
639
+
640
+ **Label and focus (since 1.20.0).** The `label` is tied to the field: it names it for screen readers, and a click on it focuses it — as do `element.focus()` and a `<label for>` naming the element. An outer `<label for>` focuses, but cannot name, the field inside the shadow root; use `label` for the name.
661
641
 
662
642
  ---
663
643
 
@@ -797,6 +777,8 @@ it happens to be first.
797
777
  A `value` naming no existing option leaves the current selection alone rather than
798
778
  clearing it — when the matching option arrives later, it is selected then.
799
779
 
780
+ **Set as a property (since 1.22.0).** The same holds for `select.value = 'b'`: a value set before the element is connected, before its options are copied in, or before the options exist at all is kept as the wanted value and selected as soon as an option carries it — until the user picks another, which replaces it. `select.value` reads it back meanwhile. Before 1.22.0 such a value threw before connecting and was lost afterwards — the select showed its first option while the app held the other value. A form reset returns to the `value` attribute.
781
+
800
782
  ---
801
783
 
802
784
  ### 3.17 `<glk-textarea>`
@@ -816,6 +798,7 @@ Multi-line text input.
816
798
  | `value` | String | Current value |
817
799
  | `disabled` | Boolean | Disabled state |
818
800
  | `required` | Boolean | Required field |
801
+ | `readonly` | Boolean | Read-only, property `.readOnly` (since 1.22.0) |
819
802
 
820
803
  Events: `glk-input`, `glk-change`.
821
804
 
@@ -842,13 +825,7 @@ Switch-style toggle (form-associated).
842
825
 
843
826
  Events: `glk-change` → `{ checked }`. Property: `.checked`.
844
827
 
845
- **One control (since 1.20.0).** The switch is the native checkbox inside, with
846
- `role="switch"`, named by `label`. The element itself no longer carries `role`,
847
- `aria-checked` or `aria-disabled` — before 1.20.0 screen readers met a switch with a checkbox
848
- inside. Select and style on `[checked]` / `[disabled]`; in tests,
849
- `getByRole('switch', { name })` finds the input and `.click()`, `.check()` work on it.
850
- `element.click()` and a `<label>` around the element or naming it toggle it, like a native
851
- checkbox. Without `label` the element is exactly its track, 52 × 30 px (it was 64 × 34).
828
+ **One control (since 1.20.0).** The switch is the native checkbox inside, with `role="switch"`, named by `label`. The element itself no longer carries `role`, `aria-checked` or `aria-disabled` — before 1.20.0 screen readers met a switch with a checkbox inside. Select and style on `[checked]` / `[disabled]`; in tests, `getByRole('switch', { name })` finds the input and `.click()`, `.check()` work on it. `element.click()` and a `<label>` around the element or naming it toggle it, like a native checkbox. Without `label` the element is exactly its track, 52 × 30 px (it was 64 × 34).
852
829
 
853
830
  ---
854
831
 
@@ -883,17 +860,11 @@ Slots:
883
860
 
884
861
  Methods: `.show()`, `.close()`. Property: `.open`. Events: `glk-close`.
885
862
 
886
- Closes automatically on overlay click and <kbd>Escape</kbd>, with its fade, and emits
887
- `glk-close` then; `.close()` and removing `open` do not emit.
863
+ Closes automatically on overlay click and <kbd>Escape</kbd>, with its fade, and emits `glk-close` then; `.close()` and removing `open` do not emit.
888
864
 
889
- **A native `<dialog>` (since 1.20.0).** The overlay is opened with `showModal()`: it lies in
890
- the top layer, the page behind it is inert for pointer, keyboard and screen readers, focus
891
- moves in and returns to where it came from on close, and the content is out of reach while
892
- closed. Chrome focuses the first field of the content, Safari the first action — put
893
- `autofocus` on the element that should get the focus, in the content or on an action.
865
+ **A native `<dialog>` (since 1.20.0).** The overlay is opened with `showModal()`: it lies in the top layer, the page behind it is inert for pointer, keyboard and screen readers, focus moves in and returns to where it came from on close, and the content is out of reach while closed. Chrome focuses the first field of the content, Safari the first action — put `autofocus` on the element that should get the focus, in the content or on an action.
894
866
 
895
- While it is open, **everything outside is inert** — a `<glk-toast>` shown then is neither
896
- clickable nor announced. Give feedback inside the modal, or close it first.
867
+ While it is open, **everything outside is inert** — a `<glk-toast>` shown then is neither clickable nor announced. Give feedback inside the modal, or close it first.
897
868
 
898
869
  ---
899
870
 
@@ -924,8 +895,7 @@ Slots:
924
895
  Methods: `.show()`, `.close()`, `.toggle()`. Property: `.open`.
925
896
  Events: `glk-open`, `glk-close`.
926
897
 
927
- The trigger gets `aria-expanded`, kept in step with `open` and set again when the trigger is
928
- swapped (since 1.20.0); `<glk-button>` and `<glk-pill>` hand it on to their native button.
898
+ The trigger gets `aria-expanded`, kept in step with `open` and set again when the trigger is swapped (since 1.20.0); `<glk-button>` and `<glk-pill>` hand it on to their native button.
929
899
 
930
900
  **Naming warning:** the toggle method is deliberately called `.toggle()`, **not** `.togglePopover()` — the latter collides with the native `HTMLElement.togglePopover()` API from the HTML Popover specification.
931
901
 
@@ -1013,9 +983,7 @@ Collapsible sections.
1013
983
 
1014
984
  Default slot: body content. Events: `glk-toggle` → `{ open }`. Property: `.open`.
1015
985
 
1016
- The trigger carries `aria-expanded` and `aria-controls`; a closed section is `inert`, so its
1017
- links and fields are out of the tab order and the accessibility tree until it opens (since
1018
- 1.20.0 — before, they were only squeezed to zero height and Tab still reached them).
986
+ The trigger carries `aria-expanded` and `aria-controls`; a closed section is `inert`, so its links and fields are out of the tab order and the accessibility tree until it opens (since 1.20.0 — before, they were only squeezed to zero height and Tab still reached them).
1019
987
 
1020
988
  ---
1021
989
 
@@ -1518,12 +1486,12 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
1518
1486
  | `<glk-title>` | Content | — | default | — |
1519
1487
  | `<glk-button>` | Buttons | `variant`, `size`, `disabled`, `type` | default | `glk-click` |
1520
1488
  | `<glk-checkbox>` | Forms | `label`, `checked`, `disabled`, `name`, `value`, `required` | — | `glk-change` |
1521
- | `<glk-input>` | Forms | `label`, `type`, `placeholder`, `hint`, `error`, `disabled`, `required`, `name`, `value`, `min`, `max`, `step`, `minlength`, `maxlength`, `pattern`, `autocomplete`, `inputmode` | — | `glk-input`, `glk-change` |
1489
+ | `<glk-input>` | Forms | `label`, `type`, `placeholder`, `hint`, `error`, `disabled`, `readonly`, `required`, `name`, `value`, `min`, `max`, `step`, `minlength`, `maxlength`, `pattern`, `autocomplete`, `inputmode` | `prefix`, `suffix` | `glk-input`, `glk-change` |
1522
1490
  | `<glk-radio>` | Forms | `label`, `name`, `value`, `checked`, `disabled`, `required` | — | `glk-change` |
1523
1491
  | `<glk-range>` | Forms | `label`, `min`, `max`, `value`, `step`, `name`, `disabled` | — | `glk-input`, `glk-change` |
1524
1492
  | `<glk-search>` | Forms | `placeholder`, `label`, `value`, `name`, `disabled` | — | `glk-input`, `glk-change` |
1525
1493
  | `<glk-select>` | Forms | `label`, `name`, `value`, `disabled`, `required` | default (`<option>`) | `glk-change` |
1526
- | `<glk-textarea>` | Forms | `label`, `rows`, `placeholder`, `name`, `value`, `disabled`, `required` | — | `glk-input`, `glk-change` |
1494
+ | `<glk-textarea>` | Forms | `label`, `rows`, `placeholder`, `name`, `value`, `disabled`, `readonly`, `required` | — | `glk-input`, `glk-change` |
1527
1495
  | `<glk-toggle>` | Forms | `label`, `checked`, `disabled`, `name`, `value`, `required` | — | `glk-change` |
1528
1496
  | `<glk-modal>` | Feedback | `open`, `title` | default, `actions` | `glk-close` |
1529
1497
  | `<glk-popover>` | Feedback | `open`, `placement` | `trigger`, default | `glk-open`, `glk-close` |
@@ -1540,6 +1508,8 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
1540
1508
 
1541
1509
  GlassKit Elements are standards-compliant Custom Elements. They work in every major framework:
1542
1510
 
1511
+ **Properties set early are kept (since 1.22.0).** Frameworks bind values as properties right after creating an element — before it is connected, and sometimes before the bundle has even defined it. A property set before the element rendered waits for it (`value`, `checked` and the rest no longer throw), and one set before the class was defined, which lands on the instance and hides the setter, is taken over through the setter on first connect.
1512
+
1543
1513
  ### Vanilla HTML / JS
1544
1514
 
1545
1515
  ```html
@@ -225,6 +225,9 @@ class GlkElement extends HTMLElement {
225
225
 
226
226
  connectedCallback() {
227
227
  if (!this._initialized) {
228
+ // Before _initialized: setters that set attributes must not reach
229
+ // onAttributeChanged yet — render() reads the attributes anyway.
230
+ this._upgradeProperties();
228
231
  this._initialized = true;
229
232
 
230
233
  // Create theme wrapper (display:contents makes it layout-transparent)
@@ -259,6 +262,30 @@ class GlkElement extends HTMLElement {
259
262
  }
260
263
  }
261
264
 
265
+ /**
266
+ * A property set on the element before its class was defined — markup a
267
+ * framework fills before the bundle has loaded — lands on the instance
268
+ * and hides the class's accessor: the setter never ran, and the value was
269
+ * lost (a glk-select kept its first option, a glk-progress stayed at 0).
270
+ * Such a value is taken off the instance and set again through the
271
+ * setter, which keeps what it cannot apply before render().
272
+ */
273
+ _upgradeProperties() {
274
+ for (const key of Object.keys(this)) {
275
+ let proto = Object.getPrototypeOf(this);
276
+ let setter = null;
277
+ while (proto && proto !== HTMLElement.prototype) {
278
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
279
+ if (desc) { setter = desc.set; break; }
280
+ proto = Object.getPrototypeOf(proto);
281
+ }
282
+ if (!setter) continue;
283
+ const value = this[key];
284
+ delete this[key];
285
+ this[key] = value;
286
+ }
287
+ }
288
+
262
289
  disconnectedCallback() {
263
290
  instances.delete(this);
264
291
  this._lightDomObserver?.disconnect();
@@ -93,11 +93,12 @@ class GlkCheckbox extends GlkFormElement {
93
93
  this._syncFormValue();
94
94
  }
95
95
 
96
- get checked() { return this._input?.checked ?? false; }
96
+ // Before render() the attribute carries the state: render() reads it.
97
+ get checked() { return this._input ? this._input.checked : this.getBoolAttr('checked'); }
97
98
  set checked(v) {
98
99
  if (this._input) this._input.checked = v;
99
100
  this.setBoolAttr('checked', v);
100
- this._syncFormValue();
101
+ if (this._input) this._syncFormValue();
101
102
  }
102
103
 
103
104
  get disabled() { return this.getBoolAttr('disabled'); }
@@ -7,11 +7,26 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
7
7
  // 1.17.0 — before, a date field with min still offered every past day).
8
8
  const FORWARDED = ['min', 'max', 'step', 'minlength', 'maxlength', 'pattern', 'autocomplete', 'inputmode'];
9
9
 
10
+ // Prefix and suffix (since 1.22.0): two slots inside the field's box, on
11
+ // GlassKit's .glass-input-wrap. An affix shows only while something is
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.
17
+ const affixSheet = new CSSStyleSheet();
18
+ affixSheet.replaceSync(`
19
+ .glass-input-wrap__prefix ::slotted(*),
20
+ .glass-input-wrap__suffix ::slotted(*) { pointer-events: auto; }
21
+ `);
22
+
10
23
  class GlkInput extends GlkFormElement {
11
24
  static get observedAttributes() {
12
- return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required', ...FORWARDED];
25
+ return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'readonly', 'name', 'value', 'required', ...FORWARDED];
13
26
  }
14
27
 
28
+ static get hostStyles() { return affixSheet; }
29
+
15
30
  render() {
16
31
  const group = this.createElement('div', ['glass-input-group']);
17
32
 
@@ -33,19 +48,28 @@ class GlkInput extends GlkFormElement {
33
48
  const name = this.getAttribute('name');
34
49
  if (name) this._input.setAttribute('name', name);
35
50
 
36
- const value = this.getAttribute('value');
51
+ // A value set as a property before render() has waited for it.
52
+ const value = this._pendingValue ?? this.getAttribute('value');
53
+ this._pendingValue = undefined;
37
54
  if (value) this._input.value = value;
38
55
 
39
56
  if (this.getBoolAttr('disabled')) this._input.disabled = true;
57
+ if (this.getBoolAttr('readonly')) this._input.readOnly = true;
40
58
  if (this.getBoolAttr('required')) this._input.required = true;
41
59
  for (const attr of FORWARDED) this._forward(attr);
42
60
 
61
+ // The field's box: the input, a prefix and a suffix slot on top of it
62
+ this._box = this.createElement('div', ['glass-input-wrap'], { part: 'box' });
63
+ this._prefix = this._createAffix('prefix');
64
+ this._suffix = this._createAffix('suffix');
65
+ this._box.append(this._prefix, this._input, this._suffix);
66
+
43
67
  // Hint — read out with the field as its description
44
68
  this._hintEl = this.createElement('span', this._computeHintClasses(), { id: 'hint' });
45
69
  this._hintEl.textContent = this.getAttribute('hint') || '';
46
70
 
47
71
  group.appendChild(this._labelEl);
48
- group.appendChild(this._input);
72
+ group.appendChild(this._box);
49
73
  if (this.getAttribute('hint')) group.appendChild(this._hintEl);
50
74
 
51
75
  this._group = group;
@@ -55,6 +79,12 @@ class GlkInput extends GlkFormElement {
55
79
  this._syncFormValue();
56
80
  }
57
81
 
82
+ _createAffix(name) {
83
+ const affix = this.createElement('span', [`glass-input-wrap__${name}`], { id: name, part: name, hidden: '' });
84
+ affix.appendChild(this.createElement('slot', [], { name }));
85
+ return affix;
86
+ }
87
+
58
88
  setupEvents() {
59
89
  this._onInput = () => {
60
90
  this._syncFormValue();
@@ -67,11 +97,26 @@ class GlkInput extends GlkFormElement {
67
97
  };
68
98
  this._input.addEventListener('input', this._onInput);
69
99
  this._input.addEventListener('change', this._onChangeNative);
100
+
101
+ // Affixes: shown while something is slotted, measured while shown.
102
+ this._onSlotChange = () => this._applyAffixes();
103
+ this._box.addEventListener('slotchange', this._onSlotChange);
104
+ this._affixObserver = new ResizeObserver(entries => {
105
+ for (const entry of entries) {
106
+ const size = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
107
+ this._box.style.setProperty(`--gl-input-${entry.target.id}-size`, `${size}px`);
108
+ }
109
+ });
110
+ this._affixObserver.observe(this._prefix);
111
+ this._affixObserver.observe(this._suffix);
112
+ this._applyAffixes();
70
113
  }
71
114
 
72
115
  teardownEvents() {
73
116
  this._input?.removeEventListener('input', this._onInput);
74
117
  this._input?.removeEventListener('change', this._onChangeNative);
118
+ this._box?.removeEventListener('slotchange', this._onSlotChange);
119
+ this._affixObserver?.disconnect();
75
120
  }
76
121
 
77
122
  onAttributeChanged(name) {
@@ -101,6 +146,9 @@ class GlkInput extends GlkFormElement {
101
146
  case 'disabled':
102
147
  this._input.disabled = this.getBoolAttr('disabled');
103
148
  break;
149
+ case 'readonly':
150
+ this._input.readOnly = this.getBoolAttr('readonly');
151
+ break;
104
152
  case 'name':
105
153
  this._input.setAttribute('name', this.getAttribute('name') || '');
106
154
  break;
@@ -116,14 +164,27 @@ class GlkInput extends GlkFormElement {
116
164
  }
117
165
  }
118
166
 
167
+ /** An affix shows, and makes room in the field, only while something is slotted. */
168
+ _applyAffixes() {
169
+ for (const affix of [this._prefix, this._suffix]) {
170
+ const filled = affix.firstElementChild.assignedNodes().length > 0;
171
+ affix.hidden = !filled;
172
+ this._box.classList.toggle(`glass-input-wrap--${affix.id}`, filled);
173
+ }
174
+ this._applyDescription();
175
+ }
176
+
119
177
  /**
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.
178
+ * Prefix, suffix and hint describe the field — a unit belongs to the value
179
+ * it stands next to. With `error` set the hint is the error message and the
180
+ * field is marked invalid. Server-side errors arrive this way, so `error`
181
+ * stays a marker for assistive technology and does not block the form —
182
+ * only the field's own constraints (required, pattern …) do.
124
183
  */
125
184
  _applyDescription() {
126
- if (this.getAttribute('hint')) this._input.setAttribute('aria-describedby', 'hint');
185
+ const ids = [this._prefix, this._suffix].filter(affix => affix && !affix.hidden).map(affix => affix.id);
186
+ if (this.getAttribute('hint')) ids.push('hint');
187
+ if (ids.length) this._input.setAttribute('aria-describedby', ids.join(' '));
127
188
  else this._input.removeAttribute('aria-describedby');
128
189
  if (this.getBoolAttr('error')) this._input.setAttribute('aria-invalid', 'true');
129
190
  else this._input.removeAttribute('aria-invalid');
@@ -158,15 +219,22 @@ class GlkInput extends GlkFormElement {
158
219
  this._syncFormValue();
159
220
  }
160
221
 
161
- get value() { return this._input?.value ?? ''; }
222
+ get value() { return this._input ? this._input.value : (this._pendingValue ?? this.getAttribute('value') ?? ''); }
162
223
  set value(v) {
163
- if (this._input) this._input.value = v;
224
+ const value = v == null ? '' : String(v);
225
+ // Frameworks set properties right after creating the element. Before
226
+ // render() the value waits for it — it used to throw.
227
+ if (!this._input) { this._pendingValue = value; return; }
228
+ this._input.value = value;
164
229
  this._syncFormValue();
165
230
  }
166
231
 
167
232
  get disabled() { return this.getBoolAttr('disabled'); }
168
233
  set disabled(v) { this.setBoolAttr('disabled', v); }
169
234
 
235
+ get readOnly() { return this.getBoolAttr('readonly'); }
236
+ set readOnly(v) { this.setBoolAttr('readonly', v); }
237
+
170
238
  get name() { return this.getAttribute('name'); }
171
239
  set name(v) { this.setAttribute('name', v); }
172
240
  }
@@ -190,14 +190,15 @@ class GlkRadio extends GlkFormElement {
190
190
  syncGroupTabIndex(this._group());
191
191
  }
192
192
 
193
- get checked() { return this._input?.checked ?? false; }
193
+ // Before render() the attribute carries the state: render() reads it.
194
+ get checked() { return this._input ? this._input.checked : this.getBoolAttr('checked'); }
194
195
  set checked(v) {
195
196
  if (this._input) this._input.checked = v;
196
197
  // Also covers the case where the attribute is already present, so
197
198
  // setBoolAttr stays silent and onAttributeChanged never runs.
198
199
  if (v) this._uncheckPeers();
199
200
  this.setBoolAttr('checked', v);
200
- this._syncFormValue();
201
+ if (this._input) this._syncFormValue();
201
202
  syncGroupTabIndex(this._group());
202
203
  }
203
204
 
@@ -36,13 +36,17 @@ class GlkRange extends GlkFormElement {
36
36
 
37
37
  if (this.getBoolAttr('disabled')) this._input.disabled = true;
38
38
 
39
+ // The reset value is the attribute's; a value set as a property before
40
+ // render() has waited for it and comes on top.
41
+ this._defaultValue = this._input.value;
42
+ if (this._pendingValue !== undefined) this._input.value = this._pendingValue;
43
+ this._pendingValue = undefined;
39
44
  this._updateValueDisplay();
40
45
 
41
46
  group.appendChild(header);
42
47
  group.appendChild(this._input);
43
48
 
44
49
  this._wrapper.appendChild(group);
45
- this._defaultValue = this._input.value;
46
50
  this._syncFormValue();
47
51
  }
48
52
 
@@ -103,9 +107,13 @@ class GlkRange extends GlkFormElement {
103
107
  this._syncFormValue();
104
108
  }
105
109
 
106
- get value() { return this._input?.value ?? ''; }
110
+ get value() { return this._input ? this._input.value : (this._pendingValue ?? this.getAttribute('value') ?? '50'); }
107
111
  set value(v) {
108
- if (this._input) this._input.value = v;
112
+ const value = v == null ? '' : String(v);
113
+ // Frameworks set properties right after creating the element. Before
114
+ // render() the value waits for it — it used to throw.
115
+ if (!this._input) { this._pendingValue = value; return; }
116
+ this._input.value = value;
109
117
  this._updateValueDisplay();
110
118
  this._syncFormValue();
111
119
  }