@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 +26 -1
- package/README.md +5 -5
- package/SKILL.md +16 -10
- package/dist/components/glk-accordion-item.js +9 -4
- package/dist/components/glk-modal.js +11 -0
- package/dist/components/glk-sheet.js +91 -40
- package/dist/components/shared/scroll-lock-CPPPbHHK.js +44 -0
- package/dist/glasskit-elements.esm.js +154 -46
- package/dist/glasskit-elements.js +154 -46
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/containers/glk-accordion-item.js +9 -4
- package/src/components/feedback/glk-modal.js +11 -0
- package/src/components/feedback/glk-sheet.js +91 -40
- package/src/scroll-lock.js +42 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
15
|
-
// .
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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('
|
|
31
|
-
this._panel = this.createElement('div', ['glass-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
|
-
|
|
52
|
-
|
|
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.
|
|
64
|
-
|
|
65
|
-
this.
|
|
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.
|
|
68
|
-
this._overlay.
|
|
81
|
+
this._panel.removeAttribute('role');
|
|
82
|
+
this._overlay.appendChild(this._panel);
|
|
69
83
|
}
|
|
84
|
+
this._applyTitle();
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
|
|
87
|
+
/** Drops a pending close. */
|
|
88
|
+
_settle() {
|
|
73
89
|
clearTimeout(this._hideTimer);
|
|
74
|
-
this._panel.removeEventListener('transitionend', this._onHidden);
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
104
|
+
if (this.open && !this.inline) dialog.classList.add('is-active');
|
|
79
105
|
});
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
_hide() {
|
|
83
|
-
this._overlay
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
133
|
+
if (event.target === this._overlay) this._dismiss();
|
|
105
134
|
};
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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':
|
|
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 };
|