@jungherz-de/glasskit-elements 1.29.1 → 1.30.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,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.30.0] – 2026-10-22
11
+
12
+ Versions stay aligned with GlassKit at 1.30.0, which this release is built against (peer dependency `>=1.30.0`).
13
+
14
+ ### Changed
15
+
16
+ - **`<glk-sheet>` is a native `<dialog>` in the top layer.** Its overlay was a `div` with `position: fixed` and `z-index: 1000` in its shadow root, so whether it covered the page depended on stacking contexts and on how the browser composites: on an iPhone a fixed bottom tab bar with a backdrop filter and a transform now and then landed above an open sheet. Since 1.30.0 the overlay is a `<dialog>` opened with `showModal()`, as `<glk-modal>`'s has been since 1.20.0: it lies in the top layer, above every fixed or transformed element whatever its `z-index`, the page behind it is inert for pointer, keyboard and screen readers, focus moves in — an `autofocus` element takes it — and Escape arrives as the dialog's `cancel` event and slides it out. The slide and the fade stay as they were, on GlassKit 1.30.0's rules for `dialog.glass-sheet-overlay`; `inline` keeps the panel in the flow, and switching between the two works while open. The part `overlay` is the `<dialog>` now; the title names the dialog through `aria-labelledby`. ([#21](https://github.com/JUNGHERZ/GlassKit-Elements/issues/21))
17
+
18
+ ### Added
19
+
20
+ - **`glk-open` on `<glk-sheet>` and `<glk-modal>`.** Nobody learned that a sheet had opened: only `glk-close` existed, and only for the visitor's close. Both now emit `glk-open` whenever they open — attribute, property or `show()`, also when they start open — so a page can hide its own fixed bars, for instance. `glk-close` keeps its meaning. ([#21](https://github.com/JUNGHERZ/GlassKit-Elements/issues/21))
21
+ - **The page behind an open sheet or modal does not scroll.** The top layer made the page inert for pointer and keyboard, but a wheel or a finger over the dimmed area still scrolled it, and on a phone touch scrolling moved the page and slid its bars. While either is open, the root scroller takes `overflow: hidden`, and a classic scrollbar keeps its room so the page does not shift; the lock is shared, so it ends only when the last open sheet or modal closes. Scripts of the page can still scroll it. ([#21](https://github.com/JUNGHERZ/GlassKit-Elements/issues/21))
22
+ - **Parts on `<glk-accordion-item>`:** `item`, `trigger`, `content`, `body`. Its shadow root had none, so a page could not reach the content at all. ([#20](https://github.com/JUNGHERZ/GlassKit-Elements/issues/20))
23
+
24
+ ### Fixed
25
+
26
+ - **An open `<glk-accordion-item>` shows all of its content.** GlassKit capped the open content at `max-height: 500px` with `overflow: hidden`, so a section with 40 rows of 30px showed 500 of 1,220px and the rest, a "show more" row included, could not be reached. GlassKit 1.30.0 slides a grid row from `0fr` to `1fr` instead: the section takes the height of its content, however long, also when the content grows while open, and the slide stays. ([#20](https://github.com/JUNGHERZ/GlassKit-Elements/issues/20))
27
+ - **Escape pressed twice on `<glk-modal>` emitted `glk-close` twice.** The second press arrived while the modal faded out; it is ignored now, on the sheet as well.
28
+ - **A modal or sheet with nothing to focus no longer rings the screen.** `showModal()` then focuses the dialog itself, and Chromium and WebKit drew its focus ring along the edges of the viewport; GlassKit 1.30.0 gives both dialogs no outline. Tab still goes to the first control.
29
+
30
+ Measured in Chromium 153, WebKit 26.6 and Firefox 151, with the issues' pages: the accordion item shows 1,220 of 1,220px (1.29.1: 500) and has its four parts. The sheet matches `:modal` after `setAttribute('open', '')`, `glk-open` fires once, the `autofocus` field has the focus, the root takes `overflow: hidden` and a wheel over the scrim leaves the page where it was (1.29.1: no event, focus on the body, the page scrolled); Escape and a scrim click slide it out with one `glk-close`, `close()` with none; a modal and a sheet open together keep the lock until the second closes; a sheet moved while open stays modal; an inline sheet opens no dialog and locks nothing, and turned into an overlay while open it becomes modal with `glk-open`. The modal emits `glk-open` and locks the page as well, and two Escapes give one `glk-close` (1.29.1: two). An open sheet renders pixel-identical to the `div` of 1.29.1, light and dark, 390 and 800px wide.
31
+
32
+ ---
33
+
10
34
  ## [1.29.1] – 2026-10-21
11
35
 
12
36
  Rebuilt against GlassKit 1.29.1; the peer dependency moves to `>=1.29.1`.
@@ -956,7 +980,8 @@ Starting with this release, GlassKit Elements version numbers are aligned with G
956
980
 
957
981
  ---
958
982
 
959
- [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.29.1...HEAD
983
+ [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.30.0...HEAD
984
+ [1.30.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.30.0
960
985
  [1.29.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.29.1
961
986
  [1.29.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.29.0
962
987
  [1.28.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.28.1
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.29.1-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.30.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
 
@@ -67,7 +67,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
67
67
 
68
68
  ```html
69
69
  <!-- 1. GlassKit CSS -->
70
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.29/glasskit.min.css">
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.30/glasskit.min.css">
71
71
 
72
72
  <!-- 2. GlassKit Elements -->
73
73
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
@@ -105,7 +105,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
105
105
  <!DOCTYPE html>
106
106
  <html data-theme="dark">
107
107
  <head>
108
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.29/glasskit.min.css">
108
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.30/glasskit.min.css">
109
109
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
110
110
  </head>
111
111
  <body>
@@ -174,10 +174,10 @@ All form components support `name`, `value`, `disabled` and participate in nativ
174
174
  | Component | Description | Key Attributes |
175
175
  |---|---|---|
176
176
  | `<glk-progress>` | Progress bar | `value`, `label`, `variant` (success, error), `size` (sm, lg) |
177
- | `<glk-modal>` | Modal dialog — a native `<dialog>`: page behind inert, focus in and back | `open`, `title` |
177
+ | `<glk-modal>` | Modal dialog — a native `<dialog>`: page behind inert and still, focus in and back | `open`, `title` |
178
178
  | `<glk-toast>` | Notification that hides itself, or an offer with one action and an × | `message`, `variant` (success, error, warning), `duration`, `action-label`, `action-value`, `dismissible` |
179
179
  | `<glk-popover>` | Anchored dropdown / menu | `open`, `placement` (top, bottom, start, end) |
180
- | `<glk-sheet>` | Bottom sheet, the mobile sibling of the modal | `open`, `inline`, `title` |
180
+ | `<glk-sheet>` | Bottom sheet, the mobile sibling of the modal — a native `<dialog>` in the top layer since 1.30.0 | `open`, `inline`, `title` |
181
181
 
182
182
  ### Containers
183
183
 
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.29.1) wrapping GlassKit CSS v1.29.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.30.0) wrapping GlassKit CSS v1.30.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
@@ -286,6 +286,8 @@ container components are exposed as parts:
286
286
  | `<glk-card>` | `card` |
287
287
  | `<glk-list>` | `header`, `list` |
288
288
  | `<glk-modal>` | `overlay`, `modal`, `header`, `body`, `footer` |
289
+ | `<glk-sheet>` | `overlay`, `sheet`, `title`, `body`, `actions` |
290
+ | `<glk-accordion-item>` | `item`, `trigger`, `content`, `body` (since 1.30.0) |
289
291
 
290
292
  ```css
291
293
  glk-card::part(card) { border-radius: 8px; }
@@ -917,7 +919,9 @@ Slots:
917
919
  - default — modal body content
918
920
  - `actions` — container for footer buttons (GlassKit expects `<button class="glass-modal__action">`)
919
921
 
920
- Methods: `.show()`, `.close()`. Property: `.open`. Events: `glk-close`.
922
+ Methods: `.show()`, `.close()`. Property: `.open`. Events: `glk-open` — whenever it opens (since 1.30.0); `glk-close` — the user closed it.
923
+
924
+ While it is open the page behind it does not scroll (since 1.30.0): the top layer kept it inert for pointer and keyboard, but a wheel or a finger over the dimmed area still moved it. The lock is shared with `<glk-sheet>`: it ends when the last of them closes. Scripts of the page can still scroll it.
921
925
 
922
926
  Closes automatically on overlay click and <kbd>Escape</kbd>, with its fade, and emits `glk-close` then; `.close()` and removing `open` do not emit.
923
927
 
@@ -1042,10 +1046,12 @@ Collapsible sections.
1042
1046
  | `title` | String | Trigger label |
1043
1047
  | `open` | Boolean | Expanded state |
1044
1048
 
1045
- Default slot: body content. Events: `glk-toggle` → `{ open }`. Property: `.open`.
1049
+ Default slot: body content. Events: `glk-toggle` → `{ open }`. Property: `.open`. Parts: `item`, `trigger`, `content`, `body` (since 1.30.0).
1046
1050
 
1047
1051
  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).
1048
1052
 
1053
+ An open section takes the height of its content, however long, and slides to it (GlassKit 1.30.0: a grid row from `0fr` to `1fr`). Until then it ended at 500px and cut off the rest — a list of 40 rows showed 500 of 1,220px. Never lift a cap with `max-height` of your own; there is none to lift.
1054
+
1049
1055
  ---
1050
1056
 
1051
1057
  ### 3.24 `<glk-list>` + `<glk-list-item>`
@@ -1223,7 +1229,7 @@ Part: `steps`.
1223
1229
 
1224
1230
  ### 3.27 `<glk-sheet>`
1225
1231
 
1226
- Bottom sheet (since 1.15.0) — the mobile sibling of `<glk-modal>`, same API shape. Opening and closing are animated by GlassKit's `.glass-sheet` block: `.is-active` is added one frame after the overlay is unhidden, and on closing the overlay is hidden only after `transitionend` (safety net 400 ms), so no blurred layer idles behind the page; under `prefers-reduced-motion` it switches at once. The Escape listener sits on `document` and is removed in `teardownEvents()`.
1232
+ Bottom sheet (since 1.15.0) — the mobile sibling of `<glk-modal>`, same API shape. **Since 1.30.0 a native `<dialog>` opened with `showModal()`**, like the modal: it lies in the top layer, above every fixed or transformed element of the page whatever its `z-index` (on iOS a fixed tab bar with a backdrop filter could land above the old fixed `div`), the page behind it is inert and does not scroll, focus moves in (`autofocus` decides where), and Escape arrives as the dialog's `cancel` event. Opening and closing are animated by GlassKit's `.glass-sheet` block: `.is-active` is added one frame after `showModal()`, and on closing the dialog closes only after the panel's `transitionend` (safety net 400 ms); under `prefers-reduced-motion` it switches at once. `inline` keeps the panel in the flow; the dialog stays closed then.
1227
1233
 
1228
1234
  ```html
1229
1235
  <glk-sheet id="sheet" title="Rebook to …">
@@ -1237,9 +1243,9 @@ Bottom sheet (since 1.15.0) — the mobile sibling of `<glk-modal>`, same API sh
1237
1243
  |---|---|---|
1238
1244
  | `open` | Boolean | Shown as an overlay (reflected property) |
1239
1245
  | `inline` | Boolean | In the flow, no overlay, always visible — e.g. inside a phone frame or a card |
1240
- | `title` | String | Heading and `aria-label` of the dialog |
1246
+ | `title` | String | Heading; names the dialog |
1241
1247
 
1242
- Methods: `show()`, `close()`. Slots: default (content), `actions`. Events: `glk-close` — only when the user closes it (scrim click, Escape), like `<glk-modal>`; `close()` does not emit. Parts: `overlay`, `sheet`, `title`, `body`, `actions`.
1248
+ Methods: `show()`, `close()`. Slots: default (content), `actions`. Events: `glk-open` — whenever it opens, by attribute, property or `show()` (since 1.30.0), for instance to hide a page's own fixed bars; `glk-close` — only when the user closes it (scrim click, Escape), like `<glk-modal>`; `close()` does not emit. Parts: `overlay` (the `<dialog>`), `sheet`, `title`, `body`, `actions`.
1243
1249
 
1244
1250
  ---
1245
1251
 
@@ -1512,12 +1518,12 @@ Key points:
1512
1518
  | `<glk-checkbox>` | `.checked` | `checked` | `glk-change` | `{ checked }` |
1513
1519
  | `<glk-radio>` | `.checked` | `checked` | `glk-change` | `{ checked, value }` |
1514
1520
  | `<glk-range>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
1515
- | `<glk-modal>` | `.open` | `open` | `glk-close` | — |
1521
+ | `<glk-modal>` | `.open` | `open` | `glk-open`, `glk-close` (user close only) | — |
1516
1522
  | `<glk-popover>` | `.open` | `open` | `glk-open`, `glk-close` | — |
1517
1523
  | `<glk-accordion-item>` | `.open` | `open` | `glk-toggle` | `{ open }` |
1518
1524
  | `<glk-list-item>` | — | `interactive` | `glk-click` (only when interactive) | — |
1519
1525
  | `<glk-segmented>` | `.value` | `value` | `glk-change` | `{ value }` |
1520
- | `<glk-sheet>` | `.open` | `open` | `glk-close` (user close only) | — |
1526
+ | `<glk-sheet>` | `.open` | `open` | `glk-open`, `glk-close` (user close only) | — |
1521
1527
  | `<glk-date-strip>` | `.value` | `value` | `glk-change` (user only) | `{ value }` |
1522
1528
  | `<glk-calendar>` | `.value`, `.month` | `value`, `month` | `glk-change` (user only), `glk-month` (user only) | `{ value }`, `{ month }` |
1523
1529
  | `<glk-image-picker>` | `.src` (not reflected) | `src` | `glk-change`, `glk-error` | `{ dataUrl, width, height, size }`, `{ message, name }` |
@@ -1580,7 +1586,7 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
1580
1586
  | `<glk-empty>` | Content | `title`, `text` | default (icon), `action` | — |
1581
1587
  | `<glk-segmented>` | Forms | `options`, `value`, `full`, `overflow`, `droplet`, `icon-only`, `label`, `name` | `icon-<value>` | `glk-change` |
1582
1588
  | `<glk-steps>` | Navigation | `steps`, `current`, `label` | — | — |
1583
- | `<glk-sheet>` | Feedback | `open`, `inline`, `title` | default, `actions` | `glk-close` |
1589
+ | `<glk-sheet>` | Feedback | `open`, `inline`, `title` | default, `actions` | `glk-open`, `glk-close` |
1584
1590
  | `<glk-date-strip>` | Navigation | `start`, `days`, `value`, `today`, `marks`, `locale`, `label` | — | `glk-change` |
1585
1591
  | `<glk-calendar>` | Forms | `month`, `value`, `today`, `min`, `max`, `marks`, `locale`, `week-start`, `label`, `name` | — | `glk-change`, `glk-month` |
1586
1592
  | `<glk-image-picker>` | Forms | `src`, `label`, `hint`, `round`, `max`, `type`, `quality`, `accept` | — | `glk-change`, `glk-error` |
@@ -1597,7 +1603,7 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
1597
1603
  | `<glk-select>` | Forms | `label`, `name`, `value`, `disabled`, `required` | default (`<option>`) | `glk-change` |
1598
1604
  | `<glk-textarea>` | Forms | `label`, `rows`, `placeholder`, `name`, `value`, `disabled`, `readonly`, `required` | — | `glk-input`, `glk-change` |
1599
1605
  | `<glk-toggle>` | Forms | `label`, `checked`, `disabled`, `name`, `value`, `required` | — | `glk-change` |
1600
- | `<glk-modal>` | Feedback | `open`, `title` | default, `actions` | `glk-close` |
1606
+ | `<glk-modal>` | Feedback | `open`, `title` | default, `actions` | `glk-open`, `glk-close` |
1601
1607
  | `<glk-popover>` | Feedback | `open`, `placement` | `trigger`, default | `glk-open`, `glk-close` |
1602
1608
  | `<glk-progress>` | Feedback | `value`, `label`, `variant`, `size` | — | — |
1603
1609
  | `<glk-toast>` | Feedback | `message`, `variant`, `duration`, `visible`, `action-label`, `action-value`, `dismissible`, `close-label` | `icon` | `glk-action`, `glk-close`, `glk-dismiss` |
@@ -3,18 +3,23 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
4
  const CHEVRON_SVG = `<span class="glass-accordion__trigger-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="6 9 12 15 18 9"/></svg></span>`;
5
5
 
6
+ // Open, the content takes its own height (GlassKit 1.30.0: a grid row that
7
+ // slides from 0fr to 1fr; until then it ended at 500 px). Parts since
8
+ // 1.30.0: item, trigger, content, body.
9
+
6
10
  class GlkAccordionItem extends GlkElement {
7
11
  static get observedAttributes() {
8
12
  return ['title', 'open'];
9
13
  }
10
14
 
11
15
  render() {
12
- this._item = this.createElement('div', ['glass-accordion__item']);
16
+ this._item = this.createElement('div', ['glass-accordion__item'], { part: 'item' });
13
17
 
14
18
  // Trigger button — says whether its section is open, and which one it is
15
19
  this._trigger = this.createElement('button', ['glass-accordion__trigger'], {
16
20
  type: 'button',
17
- 'aria-controls': 'content'
21
+ 'aria-controls': 'content',
22
+ part: 'trigger'
18
23
  });
19
24
  this.takeTitle();
20
25
  this._triggerText = document.createTextNode(this._title || '');
@@ -22,8 +27,8 @@ class GlkAccordionItem extends GlkElement {
22
27
  this._trigger.insertAdjacentHTML('beforeend', CHEVRON_SVG);
23
28
 
24
29
  // Content
25
- this._content = this.createElement('div', ['glass-accordion__content'], { id: 'content' });
26
- const body = this.createElement('div', ['glass-accordion__body']);
30
+ this._content = this.createElement('div', ['glass-accordion__content'], { id: 'content', part: 'content' });
31
+ const body = this.createElement('div', ['glass-accordion__body'], { part: 'body' });
27
32
  body.appendChild(document.createElement('slot'));
28
33
  this._content.appendChild(body);
29
34
 
@@ -1,4 +1,5 @@
1
1
  import { GlkElement } from './base.js';
2
+ import { s as scrollLock } from './shared/scroll-lock-CPPPbHHK.js';
2
3
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
4
 
4
5
  // Since 1.20.0 the overlay is a native <dialog>, opened with showModal(). The
@@ -17,6 +18,10 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
17
18
  // Chrome takes the first field of the content, Safari the first action. An
18
19
  // element marked autofocus — in the content or among the actions — settles
19
20
  // it in both.
21
+ //
22
+ // Since 1.30.0 it emits glk-open whenever it opens — attribute, property or
23
+ // show() — and the page does not scroll while it is open (glk-close stays
24
+ // the user's close only).
20
25
 
21
26
  const CLOSE_FALLBACK_MS = 400;
22
27
 
@@ -86,6 +91,7 @@ class GlkModal extends GlkElement {
86
91
 
87
92
  this._overlay.appendChild(modal);
88
93
  this._wrapper.appendChild(this._overlay);
94
+ this._scrollLock = scrollLock();
89
95
  // `open` is applied in setupEvents, which also runs when a moved element
90
96
  // comes back.
91
97
  }
@@ -137,6 +143,7 @@ class GlkModal extends GlkElement {
137
143
  this._onClose = () => {
138
144
  if (this._overlay.open) return; // a late event from an earlier close
139
145
  this._overlay.classList.remove('is-active');
146
+ this._scrollLock.release();
140
147
  if (this.open) this._dismiss();
141
148
  };
142
149
  this._overlay.addEventListener('close', this._onClose);
@@ -155,6 +162,7 @@ class GlkModal extends GlkElement {
155
162
  this._overlay?.removeEventListener('transitionend', this._onFaded);
156
163
  this._overlay?.classList.remove('is-active');
157
164
  if (this._overlay?.open) this._overlay.close();
165
+ this._scrollLock?.release();
158
166
  }
159
167
 
160
168
  onAttributeChanged(name, _old, value) {
@@ -177,8 +185,10 @@ class GlkModal extends GlkElement {
177
185
  dialog.removeEventListener('transitionend', this._onFaded);
178
186
  if (!dialog.open) {
179
187
  dialog.showModal();
188
+ this._scrollLock.hold();
180
189
  // The actions are copies in the shadow root; the originals are not shown.
181
190
  (this._footer.querySelector('[autofocus]') || this.querySelector('[autofocus]'))?.focus();
191
+ this.emit('glk-open');
182
192
  }
183
193
  void dialog.offsetHeight; // reflow, so the fade starts from the closed state
184
194
  requestAnimationFrame(() => {
@@ -205,6 +215,7 @@ class GlkModal extends GlkElement {
205
215
 
206
216
  /** Closed by the user — a click outside, Escape. */
207
217
  _dismiss() {
218
+ if (!this.open) return; // Escape again while it fades out
208
219
  this.removeAttribute('open');
209
220
  this.emit('glk-close');
210
221
  }
@@ -1,4 +1,5 @@
1
1
  import { GlkElement } from './base.js';
2
+ import { s as scrollLock } from './shared/scroll-lock-CPPPbHHK.js';
2
3
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
4
 
4
5
  // Bottom sheet — the mobile sibling of <glk-modal>. Came back from
@@ -7,15 +8,23 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
7
8
  // Attributes: open, inline (no overlay, always visible), title
8
9
  // Properties: open, inline · Methods: show(), close()
9
10
  // Slots: default (content), actions
10
- // Event: glk-close — only when the user closes it (scrim click, Escape),
11
- // like <glk-modal>
11
+ // Events: glk-open — whenever it opens: attribute, property or show()
12
+ // (since 1.30.0); glk-close — only when the user closes it
13
+ // (scrim click, Escape), like <glk-modal>
12
14
  // Parts: overlay, sheet, title, body, actions
13
15
  //
14
- // Opening and closing are animated by the CSS block: the overlay gets
15
- // .is-active one frame after it is unhidden, and on closing it is hidden
16
- // only after transitionend (safety net 400 ms), so the blurred overlay
17
- // leaves the layout instead of idling at opacity 0. Under
18
- // prefers-reduced-motion it switches at once.
16
+ // Since 1.30.0 the overlay is a native <dialog> opened with showModal(), as
17
+ // <glk-modal>'s. It lies in the top layer, above every fixed or transformed
18
+ // element of the page whatever its z-index — a fixed tab bar with a
19
+ // backdrop filter landed above the old fixed div now and then on iOS — the
20
+ // page behind it is inert and does not scroll, and Escape arrives as the
21
+ // dialog's cancel event. Inline, the panel sits in the flow and the dialog
22
+ // stays closed.
23
+ //
24
+ // Opening and closing are animated by the CSS block: .is-active is added one
25
+ // frame after showModal(), and on closing the dialog closes only after the
26
+ // panel's transitionend (safety net 400 ms). Under prefers-reduced-motion it
27
+ // switches at once.
19
28
 
20
29
  const HIDE_FALLBACK_MS = 400;
21
30
 
@@ -27,66 +36,84 @@ class GlkSheet extends GlkElement {
27
36
  static get observedAttributes() { return ['open', 'inline', 'title']; }
28
37
 
29
38
  render() {
30
- this._overlay = this.createElement('div', [], { part: 'overlay' });
31
- this._panel = this.createElement('div', ['glass-sheet'], { role: 'dialog', part: 'sheet' });
39
+ this._overlay = this.createElement('dialog', ['glass-sheet-overlay'], { part: 'overlay' });
40
+ this._panel = this.createElement('div', ['glass-sheet'], { part: 'sheet' });
32
41
  this._panel.appendChild(this.createElement('div', ['glass-sheet__grip'], { 'aria-hidden': 'true' }));
33
- this._titleEl = this.createElement('h2', ['glass-sheet__title'], { part: 'title' });
42
+ this._titleEl = this.createElement('h2', ['glass-sheet__title'], { id: 'title', part: 'title' });
34
43
  const body = this.createElement('div', ['glass-sheet__body'], { part: 'body' });
35
44
  body.appendChild(document.createElement('slot'));
36
45
  const actions = this.createElement('div', ['glass-sheet__actions'], { part: 'actions' });
37
46
  actions.appendChild(this.createElement('slot', [], { name: 'actions' }));
38
47
  this._panel.append(this._titleEl, body, actions);
39
- this._overlay.appendChild(this._panel);
40
48
  this._wrapper.appendChild(this._overlay);
49
+ this._scrollLock = scrollLock();
41
50
  this.takeTitle();
42
- this._applyTitle();
43
51
  this._applyMode();
52
+ // `open` is applied in setupEvents, which also runs when a moved element
53
+ // comes back.
44
54
  }
45
55
 
56
+ /** The title names the dialog, or the inline panel. */
46
57
  _applyTitle() {
47
58
  if (!this._titleEl) return;
48
59
  const title = this._title || '';
49
60
  this._titleEl.textContent = title;
50
61
  this._titleEl.hidden = !title;
51
- if (title) this._panel.setAttribute('aria-label', title);
52
- else this._panel.removeAttribute('aria-label');
62
+ const named = this.inline ? this._panel : this._overlay;
63
+ const other = this.inline ? this._overlay : this._panel;
64
+ other.removeAttribute('aria-labelledby');
65
+ if (title) named.setAttribute('aria-labelledby', 'title');
66
+ else named.removeAttribute('aria-labelledby');
53
67
  }
54
68
 
69
+ /** Inline the panel sits in the flow; else it lives in the dialog. */
55
70
  _applyMode() {
56
71
  const inline = this.inline;
57
- this._overlay.classList.toggle('glass-sheet-overlay', !inline);
58
72
  this._panel.classList.toggle('glass-sheet--inline', inline);
59
- this._panel.setAttribute('aria-modal', String(!inline));
60
- clearTimeout(this._hideTimer);
61
73
  if (inline) {
74
+ this._settle();
62
75
  this._overlay.classList.remove('is-active');
63
- this._overlay.hidden = false;
64
- } else if (this.open) {
65
- this._show();
76
+ if (this._overlay.open) this._overlay.close();
77
+ this._scrollLock.release();
78
+ this._panel.setAttribute('role', 'dialog');
79
+ this._wrapper.appendChild(this._panel);
66
80
  } else {
67
- this._overlay.classList.remove('is-active');
68
- this._overlay.hidden = true;
81
+ this._panel.removeAttribute('role');
82
+ this._overlay.appendChild(this._panel);
69
83
  }
84
+ this._applyTitle();
70
85
  }
71
86
 
72
- _show() {
87
+ /** Drops a pending close. */
88
+ _settle() {
73
89
  clearTimeout(this._hideTimer);
74
- this._panel.removeEventListener('transitionend', this._onHidden);
75
- this._overlay.hidden = false;
76
- void this._overlay.offsetHeight; // reflow, so the transition starts from the hidden state
90
+ if (this._onHidden) this._panel.removeEventListener('transitionend', this._onHidden);
91
+ }
92
+
93
+ _show() {
94
+ const dialog = this._overlay;
95
+ this._settle();
96
+ if (!dialog.open) {
97
+ dialog.showModal();
98
+ this._scrollLock.hold();
99
+ this.querySelector('[autofocus]')?.focus();
100
+ this.emit('glk-open');
101
+ }
102
+ void dialog.offsetHeight; // reflow, so the transition starts from the closed state
77
103
  requestAnimationFrame(() => {
78
- if (this.open && !this.inline) this._overlay.classList.add('is-active');
104
+ if (this.open && !this.inline) dialog.classList.add('is-active');
79
105
  });
80
106
  }
81
107
 
82
108
  _hide() {
83
- this._overlay.classList.remove('is-active');
84
- this._onHidden = () => {
85
- clearTimeout(this._hideTimer);
86
- this._panel.removeEventListener('transitionend', this._onHidden);
87
- if (!this.open && !this.inline) this._overlay.hidden = true;
109
+ const dialog = this._overlay;
110
+ dialog.classList.remove('is-active');
111
+ this._onHidden = (event) => {
112
+ if (event && event.target !== this._panel) return; // a transition inside the panel
113
+ this._settle();
114
+ if (!this.open && dialog.open) dialog.close();
88
115
  };
89
- if (reducedMotion()) {
116
+ if (!dialog.open || reducedMotion()) {
90
117
  this._onHidden();
91
118
  return;
92
119
  }
@@ -94,36 +121,60 @@ class GlkSheet extends GlkElement {
94
121
  this._hideTimer = setTimeout(this._onHidden, HIDE_FALLBACK_MS);
95
122
  }
96
123
 
124
+ /** Closed by the user — a click outside, Escape. */
97
125
  _dismiss() {
126
+ if (!this.open) return;
98
127
  this.removeAttribute('open');
99
128
  this.emit('glk-close');
100
129
  }
101
130
 
102
131
  setupEvents() {
103
132
  this._onOverlayClick = (event) => {
104
- if (!this.inline && event.target === this._overlay) this._dismiss();
133
+ if (event.target === this._overlay) this._dismiss();
105
134
  };
106
- this._onKeydown = (event) => {
107
- if (event.key === 'Escape' && this.open && !this.inline) this._dismiss();
135
+ // Escape: the dialog would close at once; it slides out like a click outside.
136
+ this._onCancel = (event) => {
137
+ event.preventDefault();
138
+ this._dismiss();
139
+ };
140
+ // Every close ends the lock. The browser may also close the dialog by
141
+ // itself — Chrome does on a second Escape without user activation.
142
+ this._onClose = () => {
143
+ if (this._overlay.open) return; // a late event from an earlier close
144
+ this._overlay.classList.remove('is-active');
145
+ this._scrollLock.release();
146
+ if (this.open && !this.inline) this._dismiss();
108
147
  };
109
148
  this._overlay.addEventListener('click', this._onOverlayClick);
110
- document.addEventListener('keydown', this._onKeydown);
149
+ this._overlay.addEventListener('cancel', this._onCancel);
150
+ this._overlay.addEventListener('close', this._onClose);
151
+ if (this.open && !this.inline) this._show();
111
152
  }
112
153
 
113
154
  teardownEvents() {
114
155
  this._overlay?.removeEventListener('click', this._onOverlayClick);
115
- document.removeEventListener('keydown', this._onKeydown);
156
+ this._overlay?.removeEventListener('cancel', this._onCancel);
157
+ this._overlay?.removeEventListener('close', this._onClose);
158
+ // Taken out of the document, the dialog leaves the top layer but stays
159
+ // open. Closed now, it opens as a modal again when the element comes back.
160
+ this._settle();
161
+ this._overlay?.classList.remove('is-active');
162
+ if (this._overlay?.open) this._overlay.close();
163
+ this._scrollLock?.release();
116
164
  }
117
165
 
118
166
  onAttributeChanged(name, _old, value) {
119
167
  if (!this._overlay) return;
120
168
  switch (name) {
121
169
  case 'open':
122
- if (this.inline) return;
170
+ if (this.inline || !this.isConnected) return; // setupEvents applies it on connect
123
171
  if (this.open) this._show();
124
172
  else this._hide();
125
173
  break;
126
- case 'inline': this._applyMode(); break;
174
+ case 'inline':
175
+ this._applyMode();
176
+ if (!this.inline && this.open && this.isConnected) this._show();
177
+ break;
127
178
  case 'title': if (this.takeTitle(value)) this._applyTitle(); break;
128
179
  }
129
180
  }
@@ -0,0 +1,44 @@
1
+ // Keep the page still behind an open <glk-modal> or <glk-sheet> (since
2
+ // 1.30.0). The top layer makes the page inert for pointer and keyboard, but
3
+ // a wheel or a finger over the dimmed area still scrolled the document.
4
+ // overflow: hidden on the root scroller stops that; inner scrollers of the
5
+ // page cannot be reached through the overlay, and the page's own scripts can
6
+ // still scroll it. One count for every open overlay, so closing one of two
7
+ // keeps the lock. Where a classic scrollbar disappears with it, the body
8
+ // takes its width as padding, so the page does not shift sideways; measured
9
+ // before and after, so a gutter the browser keeps is not paid twice.
10
+
11
+ let locks = 0;
12
+ let saved = null;
13
+
14
+ function lockPageScroll() {
15
+ if (locks++ > 0) return;
16
+ const root = document.documentElement, body = document.body;
17
+ const bar = window.innerWidth - root.clientWidth;
18
+ saved = { overflow: root.style.overflow, padding: body.style.paddingRight };
19
+ const padding = getComputedStyle(body).paddingRight;
20
+ root.style.overflow = 'hidden';
21
+ const gone = bar - (window.innerWidth - root.clientWidth);
22
+ if (gone > 0) body.style.paddingRight = `calc(${padding} + ${gone}px)`;
23
+ }
24
+
25
+ function unlockPageScroll() {
26
+ if (locks === 0 || --locks > 0) return;
27
+ document.documentElement.style.overflow = saved.overflow;
28
+ document.body.style.paddingRight = saved.padding;
29
+ saved = null;
30
+ }
31
+
32
+ /**
33
+ * One overlay's share of the lock: hold() while it is open, release() on
34
+ * every way it closes — both are safe to call twice.
35
+ */
36
+ function scrollLock() {
37
+ let held = false;
38
+ return {
39
+ hold() { if (!held) { held = true; lockPageScroll(); } },
40
+ release() { if (held) { held = false; unlockPageScroll(); } },
41
+ };
42
+ }
43
+
44
+ export { scrollLock as s };