@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/glasskit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the GlassKit CSS glassmorphism library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://glasskit-elements.jungherz.com",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"vanilla-js"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@jungherz-de/glasskit": ">=1.
|
|
52
|
+
"@jungherz-de/glasskit": ">=1.30.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@jungherz-de/glasskit": "^1.
|
|
55
|
+
"@jungherz-de/glasskit": "^1.30.0",
|
|
56
56
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
57
57
|
"@rollup/plugin-terser": "^0.4.0",
|
|
58
58
|
"rollup": "^4.0.0"
|
|
@@ -2,18 +2,23 @@ import { GlkElement } from '../../base.js';
|
|
|
2
2
|
|
|
3
3
|
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>`;
|
|
4
4
|
|
|
5
|
+
// Open, the content takes its own height (GlassKit 1.30.0: a grid row that
|
|
6
|
+
// slides from 0fr to 1fr; until then it ended at 500 px). Parts since
|
|
7
|
+
// 1.30.0: item, trigger, content, body.
|
|
8
|
+
|
|
5
9
|
class GlkAccordionItem extends GlkElement {
|
|
6
10
|
static get observedAttributes() {
|
|
7
11
|
return ['title', 'open'];
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
render() {
|
|
11
|
-
this._item = this.createElement('div', ['glass-accordion__item']);
|
|
15
|
+
this._item = this.createElement('div', ['glass-accordion__item'], { part: 'item' });
|
|
12
16
|
|
|
13
17
|
// Trigger button — says whether its section is open, and which one it is
|
|
14
18
|
this._trigger = this.createElement('button', ['glass-accordion__trigger'], {
|
|
15
19
|
type: 'button',
|
|
16
|
-
'aria-controls': 'content'
|
|
20
|
+
'aria-controls': 'content',
|
|
21
|
+
part: 'trigger'
|
|
17
22
|
});
|
|
18
23
|
this.takeTitle();
|
|
19
24
|
this._triggerText = document.createTextNode(this._title || '');
|
|
@@ -21,8 +26,8 @@ class GlkAccordionItem extends GlkElement {
|
|
|
21
26
|
this._trigger.insertAdjacentHTML('beforeend', CHEVRON_SVG);
|
|
22
27
|
|
|
23
28
|
// Content
|
|
24
|
-
this._content = this.createElement('div', ['glass-accordion__content'], { id: 'content' });
|
|
25
|
-
const body = this.createElement('div', ['glass-accordion__body']);
|
|
29
|
+
this._content = this.createElement('div', ['glass-accordion__content'], { id: 'content', part: 'content' });
|
|
30
|
+
const body = this.createElement('div', ['glass-accordion__body'], { part: 'body' });
|
|
26
31
|
body.appendChild(document.createElement('slot'));
|
|
27
32
|
this._content.appendChild(body);
|
|
28
33
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GlkElement } from '../../base.js';
|
|
2
|
+
import { scrollLock } from '../../scroll-lock.js';
|
|
2
3
|
|
|
3
4
|
// Since 1.20.0 the overlay is a native <dialog>, opened with showModal(). The
|
|
4
5
|
// browser then does what a modal needs: the dialog lies in the top layer, the
|
|
@@ -16,6 +17,10 @@ import { GlkElement } from '../../base.js';
|
|
|
16
17
|
// Chrome takes the first field of the content, Safari the first action. An
|
|
17
18
|
// element marked autofocus — in the content or among the actions — settles
|
|
18
19
|
// it in both.
|
|
20
|
+
//
|
|
21
|
+
// Since 1.30.0 it emits glk-open whenever it opens — attribute, property or
|
|
22
|
+
// show() — and the page does not scroll while it is open (glk-close stays
|
|
23
|
+
// the user's close only).
|
|
19
24
|
|
|
20
25
|
const CLOSE_FALLBACK_MS = 400;
|
|
21
26
|
|
|
@@ -85,6 +90,7 @@ class GlkModal extends GlkElement {
|
|
|
85
90
|
|
|
86
91
|
this._overlay.appendChild(modal);
|
|
87
92
|
this._wrapper.appendChild(this._overlay);
|
|
93
|
+
this._scrollLock = scrollLock();
|
|
88
94
|
// `open` is applied in setupEvents, which also runs when a moved element
|
|
89
95
|
// comes back.
|
|
90
96
|
}
|
|
@@ -136,6 +142,7 @@ class GlkModal extends GlkElement {
|
|
|
136
142
|
this._onClose = () => {
|
|
137
143
|
if (this._overlay.open) return; // a late event from an earlier close
|
|
138
144
|
this._overlay.classList.remove('is-active');
|
|
145
|
+
this._scrollLock.release();
|
|
139
146
|
if (this.open) this._dismiss();
|
|
140
147
|
};
|
|
141
148
|
this._overlay.addEventListener('close', this._onClose);
|
|
@@ -154,6 +161,7 @@ class GlkModal extends GlkElement {
|
|
|
154
161
|
this._overlay?.removeEventListener('transitionend', this._onFaded);
|
|
155
162
|
this._overlay?.classList.remove('is-active');
|
|
156
163
|
if (this._overlay?.open) this._overlay.close();
|
|
164
|
+
this._scrollLock?.release();
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
onAttributeChanged(name, _old, value) {
|
|
@@ -176,8 +184,10 @@ class GlkModal extends GlkElement {
|
|
|
176
184
|
dialog.removeEventListener('transitionend', this._onFaded);
|
|
177
185
|
if (!dialog.open) {
|
|
178
186
|
dialog.showModal();
|
|
187
|
+
this._scrollLock.hold();
|
|
179
188
|
// The actions are copies in the shadow root; the originals are not shown.
|
|
180
189
|
(this._footer.querySelector('[autofocus]') || this.querySelector('[autofocus]'))?.focus();
|
|
190
|
+
this.emit('glk-open');
|
|
181
191
|
}
|
|
182
192
|
void dialog.offsetHeight; // reflow, so the fade starts from the closed state
|
|
183
193
|
requestAnimationFrame(() => {
|
|
@@ -204,6 +214,7 @@ class GlkModal extends GlkElement {
|
|
|
204
214
|
|
|
205
215
|
/** Closed by the user — a click outside, Escape. */
|
|
206
216
|
_dismiss() {
|
|
217
|
+
if (!this.open) return; // Escape again while it fades out
|
|
207
218
|
this.removeAttribute('open');
|
|
208
219
|
this.emit('glk-close');
|
|
209
220
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GlkElement } from '../../base.js';
|
|
2
|
+
import { scrollLock } from '../../scroll-lock.js';
|
|
2
3
|
|
|
3
4
|
// Bottom sheet — the mobile sibling of <glk-modal>. Came back from
|
|
4
5
|
// EhrenPfoten in 1.15.0.
|
|
@@ -6,15 +7,23 @@ import { GlkElement } from '../../base.js';
|
|
|
6
7
|
// Attributes: open, inline (no overlay, always visible), title
|
|
7
8
|
// Properties: open, inline · Methods: show(), close()
|
|
8
9
|
// Slots: default (content), actions
|
|
9
|
-
//
|
|
10
|
-
//
|
|
10
|
+
// Events: glk-open — whenever it opens: attribute, property or show()
|
|
11
|
+
// (since 1.30.0); glk-close — only when the user closes it
|
|
12
|
+
// (scrim click, Escape), like <glk-modal>
|
|
11
13
|
// Parts: overlay, sheet, title, body, actions
|
|
12
14
|
//
|
|
13
|
-
//
|
|
14
|
-
// .
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// Since 1.30.0 the overlay is a native <dialog> opened with showModal(), as
|
|
16
|
+
// <glk-modal>'s. It lies in the top layer, above every fixed or transformed
|
|
17
|
+
// element of the page whatever its z-index — a fixed tab bar with a
|
|
18
|
+
// backdrop filter landed above the old fixed div now and then on iOS — the
|
|
19
|
+
// page behind it is inert and does not scroll, and Escape arrives as the
|
|
20
|
+
// dialog's cancel event. Inline, the panel sits in the flow and the dialog
|
|
21
|
+
// stays closed.
|
|
22
|
+
//
|
|
23
|
+
// Opening and closing are animated by the CSS block: .is-active is added one
|
|
24
|
+
// frame after showModal(), and on closing the dialog closes only after the
|
|
25
|
+
// panel's transitionend (safety net 400 ms). Under prefers-reduced-motion it
|
|
26
|
+
// switches at once.
|
|
18
27
|
|
|
19
28
|
const HIDE_FALLBACK_MS = 400;
|
|
20
29
|
|
|
@@ -26,66 +35,84 @@ class GlkSheet extends GlkElement {
|
|
|
26
35
|
static get observedAttributes() { return ['open', 'inline', 'title']; }
|
|
27
36
|
|
|
28
37
|
render() {
|
|
29
|
-
this._overlay = this.createElement('
|
|
30
|
-
this._panel = this.createElement('div', ['glass-sheet'], {
|
|
38
|
+
this._overlay = this.createElement('dialog', ['glass-sheet-overlay'], { part: 'overlay' });
|
|
39
|
+
this._panel = this.createElement('div', ['glass-sheet'], { part: 'sheet' });
|
|
31
40
|
this._panel.appendChild(this.createElement('div', ['glass-sheet__grip'], { 'aria-hidden': 'true' }));
|
|
32
|
-
this._titleEl = this.createElement('h2', ['glass-sheet__title'], { part: 'title' });
|
|
41
|
+
this._titleEl = this.createElement('h2', ['glass-sheet__title'], { id: 'title', part: 'title' });
|
|
33
42
|
const body = this.createElement('div', ['glass-sheet__body'], { part: 'body' });
|
|
34
43
|
body.appendChild(document.createElement('slot'));
|
|
35
44
|
const actions = this.createElement('div', ['glass-sheet__actions'], { part: 'actions' });
|
|
36
45
|
actions.appendChild(this.createElement('slot', [], { name: 'actions' }));
|
|
37
46
|
this._panel.append(this._titleEl, body, actions);
|
|
38
|
-
this._overlay.appendChild(this._panel);
|
|
39
47
|
this._wrapper.appendChild(this._overlay);
|
|
48
|
+
this._scrollLock = scrollLock();
|
|
40
49
|
this.takeTitle();
|
|
41
|
-
this._applyTitle();
|
|
42
50
|
this._applyMode();
|
|
51
|
+
// `open` is applied in setupEvents, which also runs when a moved element
|
|
52
|
+
// comes back.
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
/** The title names the dialog, or the inline panel. */
|
|
45
56
|
_applyTitle() {
|
|
46
57
|
if (!this._titleEl) return;
|
|
47
58
|
const title = this._title || '';
|
|
48
59
|
this._titleEl.textContent = title;
|
|
49
60
|
this._titleEl.hidden = !title;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const named = this.inline ? this._panel : this._overlay;
|
|
62
|
+
const other = this.inline ? this._overlay : this._panel;
|
|
63
|
+
other.removeAttribute('aria-labelledby');
|
|
64
|
+
if (title) named.setAttribute('aria-labelledby', 'title');
|
|
65
|
+
else named.removeAttribute('aria-labelledby');
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
/** Inline the panel sits in the flow; else it lives in the dialog. */
|
|
54
69
|
_applyMode() {
|
|
55
70
|
const inline = this.inline;
|
|
56
|
-
this._overlay.classList.toggle('glass-sheet-overlay', !inline);
|
|
57
71
|
this._panel.classList.toggle('glass-sheet--inline', inline);
|
|
58
|
-
this._panel.setAttribute('aria-modal', String(!inline));
|
|
59
|
-
clearTimeout(this._hideTimer);
|
|
60
72
|
if (inline) {
|
|
73
|
+
this._settle();
|
|
61
74
|
this._overlay.classList.remove('is-active');
|
|
62
|
-
this._overlay.
|
|
63
|
-
|
|
64
|
-
this.
|
|
75
|
+
if (this._overlay.open) this._overlay.close();
|
|
76
|
+
this._scrollLock.release();
|
|
77
|
+
this._panel.setAttribute('role', 'dialog');
|
|
78
|
+
this._wrapper.appendChild(this._panel);
|
|
65
79
|
} else {
|
|
66
|
-
this.
|
|
67
|
-
this._overlay.
|
|
80
|
+
this._panel.removeAttribute('role');
|
|
81
|
+
this._overlay.appendChild(this._panel);
|
|
68
82
|
}
|
|
83
|
+
this._applyTitle();
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
|
|
86
|
+
/** Drops a pending close. */
|
|
87
|
+
_settle() {
|
|
72
88
|
clearTimeout(this._hideTimer);
|
|
73
|
-
this._panel.removeEventListener('transitionend', this._onHidden);
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
if (this._onHidden) this._panel.removeEventListener('transitionend', this._onHidden);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_show() {
|
|
93
|
+
const dialog = this._overlay;
|
|
94
|
+
this._settle();
|
|
95
|
+
if (!dialog.open) {
|
|
96
|
+
dialog.showModal();
|
|
97
|
+
this._scrollLock.hold();
|
|
98
|
+
this.querySelector('[autofocus]')?.focus();
|
|
99
|
+
this.emit('glk-open');
|
|
100
|
+
}
|
|
101
|
+
void dialog.offsetHeight; // reflow, so the transition starts from the closed state
|
|
76
102
|
requestAnimationFrame(() => {
|
|
77
|
-
if (this.open && !this.inline)
|
|
103
|
+
if (this.open && !this.inline) dialog.classList.add('is-active');
|
|
78
104
|
});
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
_hide() {
|
|
82
|
-
this._overlay
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
const dialog = this._overlay;
|
|
109
|
+
dialog.classList.remove('is-active');
|
|
110
|
+
this._onHidden = (event) => {
|
|
111
|
+
if (event && event.target !== this._panel) return; // a transition inside the panel
|
|
112
|
+
this._settle();
|
|
113
|
+
if (!this.open && dialog.open) dialog.close();
|
|
87
114
|
};
|
|
88
|
-
if (reducedMotion()) {
|
|
115
|
+
if (!dialog.open || reducedMotion()) {
|
|
89
116
|
this._onHidden();
|
|
90
117
|
return;
|
|
91
118
|
}
|
|
@@ -93,36 +120,60 @@ class GlkSheet extends GlkElement {
|
|
|
93
120
|
this._hideTimer = setTimeout(this._onHidden, HIDE_FALLBACK_MS);
|
|
94
121
|
}
|
|
95
122
|
|
|
123
|
+
/** Closed by the user — a click outside, Escape. */
|
|
96
124
|
_dismiss() {
|
|
125
|
+
if (!this.open) return;
|
|
97
126
|
this.removeAttribute('open');
|
|
98
127
|
this.emit('glk-close');
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
setupEvents() {
|
|
102
131
|
this._onOverlayClick = (event) => {
|
|
103
|
-
if (
|
|
132
|
+
if (event.target === this._overlay) this._dismiss();
|
|
104
133
|
};
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
// Escape: the dialog would close at once; it slides out like a click outside.
|
|
135
|
+
this._onCancel = (event) => {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
this._dismiss();
|
|
138
|
+
};
|
|
139
|
+
// Every close ends the lock. The browser may also close the dialog by
|
|
140
|
+
// itself — Chrome does on a second Escape without user activation.
|
|
141
|
+
this._onClose = () => {
|
|
142
|
+
if (this._overlay.open) return; // a late event from an earlier close
|
|
143
|
+
this._overlay.classList.remove('is-active');
|
|
144
|
+
this._scrollLock.release();
|
|
145
|
+
if (this.open && !this.inline) this._dismiss();
|
|
107
146
|
};
|
|
108
147
|
this._overlay.addEventListener('click', this._onOverlayClick);
|
|
109
|
-
|
|
148
|
+
this._overlay.addEventListener('cancel', this._onCancel);
|
|
149
|
+
this._overlay.addEventListener('close', this._onClose);
|
|
150
|
+
if (this.open && !this.inline) this._show();
|
|
110
151
|
}
|
|
111
152
|
|
|
112
153
|
teardownEvents() {
|
|
113
154
|
this._overlay?.removeEventListener('click', this._onOverlayClick);
|
|
114
|
-
|
|
155
|
+
this._overlay?.removeEventListener('cancel', this._onCancel);
|
|
156
|
+
this._overlay?.removeEventListener('close', this._onClose);
|
|
157
|
+
// Taken out of the document, the dialog leaves the top layer but stays
|
|
158
|
+
// open. Closed now, it opens as a modal again when the element comes back.
|
|
159
|
+
this._settle();
|
|
160
|
+
this._overlay?.classList.remove('is-active');
|
|
161
|
+
if (this._overlay?.open) this._overlay.close();
|
|
162
|
+
this._scrollLock?.release();
|
|
115
163
|
}
|
|
116
164
|
|
|
117
165
|
onAttributeChanged(name, _old, value) {
|
|
118
166
|
if (!this._overlay) return;
|
|
119
167
|
switch (name) {
|
|
120
168
|
case 'open':
|
|
121
|
-
if (this.inline) return;
|
|
169
|
+
if (this.inline || !this.isConnected) return; // setupEvents applies it on connect
|
|
122
170
|
if (this.open) this._show();
|
|
123
171
|
else this._hide();
|
|
124
172
|
break;
|
|
125
|
-
case 'inline':
|
|
173
|
+
case 'inline':
|
|
174
|
+
this._applyMode();
|
|
175
|
+
if (!this.inline && this.open && this.isConnected) this._show();
|
|
176
|
+
break;
|
|
126
177
|
case 'title': if (this.takeTitle(value)) this._applyTitle(); break;
|
|
127
178
|
}
|
|
128
179
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
export function scrollLock() {
|
|
37
|
+
let held = false;
|
|
38
|
+
return {
|
|
39
|
+
hold() { if (!held) { held = true; lockPageScroll(); } },
|
|
40
|
+
release() { if (held) { held = false; unlockPageScroll(); } },
|
|
41
|
+
};
|
|
42
|
+
}
|