@jungherz-de/glasskit-elements 1.16.0 → 1.17.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/README.md +5 -5
- package/dist/components/base.js +18 -0
- package/dist/components/glk-accordion-item.js +15 -3
- package/dist/components/glk-date-strip.js +8 -16
- package/dist/components/glk-empty.js +15 -2
- package/dist/components/glk-input.js +16 -1
- package/dist/components/glk-list-item.js +15 -3
- package/dist/components/glk-modal.js +15 -3
- package/dist/components/glk-segmented.js +43 -6
- package/dist/components/glk-sheet.js +12 -3
- package/dist/components/shared/reveal-CjjzoxTa.js +21 -0
- package/dist/glasskit-elements.esm.js +177 -39
- package/dist/glasskit-elements.js +177 -39
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +18 -0
- package/src/components/containers/glk-accordion-item.js +15 -3
- package/src/components/containers/glk-list-item.js +15 -3
- package/src/components/content/glk-empty.js +15 -2
- package/src/components/feedback/glk-modal.js +15 -3
- package/src/components/feedback/glk-sheet.js +12 -3
- package/src/components/forms/glk-input.js +16 -1
- package/src/components/forms/glk-segmented.js +43 -6
- package/src/components/navigation/glk-date-strip.js +8 -16
- package/src/reveal.js +19 -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-33-7ec8e3?style=flat-square" alt="33 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.17.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.
|
|
69
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.17/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.
|
|
107
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.17/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,7 +156,7 @@ 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` |
|
|
159
|
+
| `<glk-input>` | Text input with label & hint | `label`, `type`, `placeholder`, `hint`, `error`, `required`, `min`, `max`, `step`, `maxlength`, `pattern`, `autocomplete`, `inputmode` |
|
|
160
160
|
| `<glk-textarea>` | Multi-line text input | `label`, `rows`, `placeholder` |
|
|
161
161
|
| `<glk-select>` | Dropdown select | `label` (children: `<option>`) |
|
|
162
162
|
| `<glk-search>` | Search input with icon | `placeholder` |
|
|
@@ -164,7 +164,7 @@ All form components support `name`, `value`, `disabled` and participate in nativ
|
|
|
164
164
|
| `<glk-checkbox>` | Checkbox | `label`, `checked`, `disabled` |
|
|
165
165
|
| `<glk-radio>` | Radio button | `label`, `name`, `value`, `checked` |
|
|
166
166
|
| `<glk-range>` | Range slider | `label`, `min`, `max`, `value`, `step` |
|
|
167
|
-
| `<glk-segmented>` | Small exclusive choice as one control; form-associated | `options` (JSON), `value`, `full`, `label`, `name` |
|
|
167
|
+
| `<glk-segmented>` | Small exclusive choice as one control; form-associated | `options` (JSON), `value`, `full`, `overflow` (scroll, wrap), `label`, `name` |
|
|
168
168
|
| `<glk-calendar>` | One month with a day to pick; keyboard navigation, `Intl` names; form-associated | `month`, `value`, `today`, `min`, `max`, `marks`, `locale`, `week-start`, `label`, `name` |
|
|
169
169
|
| `<glk-image-picker>` | One image with preview, resized on the client, EXIF rotation applied; not form-associated | `src`, `label`, `hint`, `round`, `max`, `type`, `quality`, `accept` |
|
|
170
170
|
|
package/dist/components/base.js
CHANGED
|
@@ -181,6 +181,24 @@ class GlkElement extends HTMLElement {
|
|
|
181
181
|
|
|
182
182
|
// ── Utility Methods ──
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* For elements whose heading arrives as `title`. That is also the global
|
|
186
|
+
* HTML attribute, and a title on the host shows as a browser tooltip — the
|
|
187
|
+
* sheet's heading floating over its form. So the value is read into
|
|
188
|
+
* this._title and the attribute is taken off the host; a later
|
|
189
|
+
* setAttribute('title') comes through attributeChangedCallback, is taken
|
|
190
|
+
* the same way and removed again, and the element's `title` accessor
|
|
191
|
+
* answers from this._title. The removal fires the callback with null,
|
|
192
|
+
* which is ignored here — only an empty string clears the heading.
|
|
193
|
+
* Returns whether a value was taken.
|
|
194
|
+
*/
|
|
195
|
+
takeTitle(value = this.getAttribute('title')) {
|
|
196
|
+
if (value === null) return false;
|
|
197
|
+
this._title = value;
|
|
198
|
+
this.removeAttribute('title');
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
184
202
|
getBoolAttr(name) {
|
|
185
203
|
return this.hasAttribute(name);
|
|
186
204
|
}
|
|
@@ -14,7 +14,8 @@ class GlkAccordionItem extends GlkElement {
|
|
|
14
14
|
|
|
15
15
|
// Trigger button
|
|
16
16
|
this._trigger = this.createElement('button', ['glass-accordion__trigger']);
|
|
17
|
-
this.
|
|
17
|
+
this.takeTitle();
|
|
18
|
+
this._triggerText = document.createTextNode(this._title || '');
|
|
18
19
|
this._trigger.appendChild(this._triggerText);
|
|
19
20
|
this._trigger.insertAdjacentHTML('beforeend', CHEVRON_SVG);
|
|
20
21
|
|
|
@@ -42,18 +43,29 @@ class GlkAccordionItem extends GlkElement {
|
|
|
42
43
|
this._trigger?.removeEventListener('click', this._onClick);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
onAttributeChanged(name) {
|
|
46
|
+
onAttributeChanged(name, _old, value) {
|
|
46
47
|
if (!this._item) return;
|
|
47
48
|
switch (name) {
|
|
48
49
|
case 'open':
|
|
49
50
|
this._item.classList.toggle('is-open', this.getBoolAttr('open'));
|
|
50
51
|
break;
|
|
51
52
|
case 'title':
|
|
52
|
-
this.
|
|
53
|
+
if (this.takeTitle(value)) this._applyTitle();
|
|
53
54
|
break;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
_applyTitle() {
|
|
59
|
+
if (this._triggerText) this._triggerText.textContent = this._title || '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
63
|
+
get title() { return this._title ?? ''; }
|
|
64
|
+
set title(v) {
|
|
65
|
+
this._title = v == null ? '' : String(v);
|
|
66
|
+
this._applyTitle();
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
get open() { return this.getBoolAttr('open'); }
|
|
58
70
|
set open(v) { this.setBoolAttr('open', v); }
|
|
59
71
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
2
|
import { f as formatters, r as resolveLocale, b as parseJsonObject, p as parseIso, i as isoDate, c as addDays, w as weekdayShort, d as isTone } from './shared/dates-CNWaJf09.js';
|
|
3
|
+
import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
|
|
3
4
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
4
5
|
|
|
5
6
|
// A row of day chips that scrolls sideways — a booking horizon, the days
|
|
@@ -85,24 +86,15 @@ class GlkDateStrip extends GlkElement {
|
|
|
85
86
|
for (const chip of this._chips()) chip.tabIndex = chip === target ? 0 : -1;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// value change glides unless the user asked for reduced motion; a size
|
|
95
|
-
// change snaps, so a settling layout never animates.
|
|
89
|
+
// Centre the chosen chip in the strip (see reveal.js). Before the first
|
|
90
|
+
// layout the strip has no width; the ResizeObserver in setupEvents centres
|
|
91
|
+
// then, and again whenever the strip's width changes — a parent that
|
|
92
|
+
// upgrades later and adds its padding, a window resize, a strip that
|
|
93
|
+
// starts out hidden. A value change glides; a size change snaps, so a
|
|
94
|
+
// settling layout never animates.
|
|
96
95
|
_reveal(behavior = 'instant') {
|
|
97
96
|
const chip = this._strip.querySelector('[aria-pressed="true"]');
|
|
98
|
-
|
|
99
|
-
if (!chip || !width) return;
|
|
100
|
-
const strip = this._strip.getBoundingClientRect();
|
|
101
|
-
const box = chip.getBoundingClientRect();
|
|
102
|
-
const left = this._strip.scrollLeft + (box.left - strip.left) - (strip.width - box.width) / 2;
|
|
103
|
-
const smooth = behavior === 'smooth' && !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
104
|
-
this._strip.scrollTo({ left: Math.max(0, left), behavior: smooth ? 'smooth' : 'instant' });
|
|
105
|
-
this._revealedAt = width;
|
|
97
|
+
if (revealCentered(this._strip, chip, behavior === 'smooth')) this._revealedAt = this._strip.clientWidth;
|
|
106
98
|
}
|
|
107
99
|
|
|
108
100
|
_applyLabel() {
|
|
@@ -25,11 +25,12 @@ class GlkEmpty extends GlkElement {
|
|
|
25
25
|
action.appendChild(this.createElement('slot', [], { name: 'action' }));
|
|
26
26
|
root.append(icon, this._titleEl, this._textEl, action);
|
|
27
27
|
this._wrapper.appendChild(root);
|
|
28
|
+
this.takeTitle();
|
|
28
29
|
this._update();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
_update() {
|
|
32
|
-
const title = this.
|
|
33
|
+
const title = this._title || '';
|
|
33
34
|
const text = this.getAttribute('text') || '';
|
|
34
35
|
this._titleEl.textContent = title;
|
|
35
36
|
this._titleEl.hidden = !title;
|
|
@@ -37,10 +38,22 @@ class GlkEmpty extends GlkElement {
|
|
|
37
38
|
this._textEl.hidden = !text;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
onAttributeChanged() {
|
|
41
|
+
onAttributeChanged(name, _old, value) {
|
|
42
|
+
if (name === 'title' && !this.takeTitle(value)) return;
|
|
41
43
|
if (this._titleEl) this._update();
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
_applyTitle() {
|
|
47
|
+
if (this._titleEl) this._update();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
51
|
+
get title() { return this._title ?? ''; }
|
|
52
|
+
set title(v) {
|
|
53
|
+
this._title = v == null ? '' : String(v);
|
|
54
|
+
this._applyTitle();
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
get text() { return this.getAttribute('text') || ''; }
|
|
45
58
|
set text(v) {
|
|
46
59
|
if (v) this.setAttribute('text', v);
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { GlkFormElement } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
|
+
// Attributes that belong to the inner <input> and are handed down unchanged:
|
|
5
|
+
// the picker's range and step, the typing limits, the pattern, autofill and
|
|
6
|
+
// the on-screen keyboard. Absent on the host means absent inside (since
|
|
7
|
+
// 1.17.0 — before, a date field with min still offered every past day).
|
|
8
|
+
const FORWARDED = ['min', 'max', 'step', 'minlength', 'maxlength', 'pattern', 'autocomplete', 'inputmode'];
|
|
9
|
+
|
|
4
10
|
class GlkInput extends GlkFormElement {
|
|
5
11
|
static get observedAttributes() {
|
|
6
|
-
return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required'];
|
|
12
|
+
return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required', ...FORWARDED];
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
render() {
|
|
@@ -29,6 +35,7 @@ class GlkInput extends GlkFormElement {
|
|
|
29
35
|
|
|
30
36
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
31
37
|
if (this.getBoolAttr('required')) this._input.required = true;
|
|
38
|
+
for (const attr of FORWARDED) this._forward(attr);
|
|
32
39
|
|
|
33
40
|
// Hint
|
|
34
41
|
this._hintEl = this.createElement('span', this._computeHintClasses());
|
|
@@ -98,9 +105,17 @@ class GlkInput extends GlkFormElement {
|
|
|
98
105
|
case 'required':
|
|
99
106
|
this._input.required = this.getBoolAttr('required');
|
|
100
107
|
break;
|
|
108
|
+
default:
|
|
109
|
+
if (FORWARDED.includes(name)) this._forward(name);
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
_forward(attr) {
|
|
114
|
+
const value = this.getAttribute(attr);
|
|
115
|
+
if (value === null) this._input.removeAttribute(attr);
|
|
116
|
+
else this._input.setAttribute(attr, value);
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
_computeInputClasses() {
|
|
105
120
|
const classes = ['glass-input'];
|
|
106
121
|
if (this.getBoolAttr('error')) classes.push('glass-input--error');
|
|
@@ -49,7 +49,8 @@ class GlkListItem extends GlkElement {
|
|
|
49
49
|
// Content — title + optional subtitle.
|
|
50
50
|
this._content = this.createElement('div', ['glass-list__content']);
|
|
51
51
|
this._titleEl = this.createElement('div', ['glass-list__title']);
|
|
52
|
-
this.
|
|
52
|
+
this.takeTitle();
|
|
53
|
+
this._titleEl.textContent = this._title || '';
|
|
53
54
|
this._subtitleEl = this.createElement('div', ['glass-list__subtitle']);
|
|
54
55
|
if (this.getBoolAttr('wrap')) this._subtitleEl.classList.add('glass-list__subtitle--wrap');
|
|
55
56
|
const initialSubtitle = this.getAttribute('subtitle') || '';
|
|
@@ -115,11 +116,11 @@ class GlkListItem extends GlkElement {
|
|
|
115
116
|
this._item?.removeEventListener('click', this._onClick);
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
onAttributeChanged(name) {
|
|
119
|
+
onAttributeChanged(name, _old, value) {
|
|
119
120
|
if (!this._item) return;
|
|
120
121
|
switch (name) {
|
|
121
122
|
case 'title':
|
|
122
|
-
this.
|
|
123
|
+
if (this.takeTitle(value)) this._applyTitle();
|
|
123
124
|
break;
|
|
124
125
|
case 'subtitle': {
|
|
125
126
|
const value = this.getAttribute('subtitle') || '';
|
|
@@ -159,6 +160,17 @@ class GlkListItem extends GlkElement {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
_applyTitle() {
|
|
164
|
+
if (this._titleEl) this._titleEl.textContent = this._title || '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
168
|
+
get title() { return this._title ?? ''; }
|
|
169
|
+
set title(v) {
|
|
170
|
+
this._title = v == null ? '' : String(v);
|
|
171
|
+
this._applyTitle();
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
get interactive() { return this.getBoolAttr('interactive'); }
|
|
163
175
|
set interactive(v) { this.setBoolAttr('interactive', v); }
|
|
164
176
|
|
|
@@ -16,7 +16,8 @@ class GlkModal extends GlkElement {
|
|
|
16
16
|
// Header
|
|
17
17
|
const header = this.createElement('div', ['glass-modal__header'], { part: 'header' });
|
|
18
18
|
this._titleEl = this.createElement('h2', ['glass-modal__title']);
|
|
19
|
-
this.
|
|
19
|
+
this.takeTitle();
|
|
20
|
+
this._applyTitle();
|
|
20
21
|
header.appendChild(this._titleEl);
|
|
21
22
|
|
|
22
23
|
// Body
|
|
@@ -85,14 +86,14 @@ class GlkModal extends GlkElement {
|
|
|
85
86
|
document.removeEventListener('keydown', this._onKeydown);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
onAttributeChanged(name) {
|
|
89
|
+
onAttributeChanged(name, _old, value) {
|
|
89
90
|
if (!this._overlay) return;
|
|
90
91
|
switch (name) {
|
|
91
92
|
case 'open':
|
|
92
93
|
this._overlay.classList.toggle('is-active', this.getBoolAttr('open'));
|
|
93
94
|
break;
|
|
94
95
|
case 'title':
|
|
95
|
-
this.
|
|
96
|
+
if (this.takeTitle(value)) this._applyTitle();
|
|
96
97
|
break;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
@@ -100,6 +101,17 @@ class GlkModal extends GlkElement {
|
|
|
100
101
|
show() { this.setAttribute('open', ''); }
|
|
101
102
|
close() { this.removeAttribute('open'); }
|
|
102
103
|
|
|
104
|
+
_applyTitle() {
|
|
105
|
+
if (this._titleEl) this._titleEl.textContent = this._title || '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
109
|
+
get title() { return this._title ?? ''; }
|
|
110
|
+
set title(v) {
|
|
111
|
+
this._title = v == null ? '' : String(v);
|
|
112
|
+
this._applyTitle();
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
get open() { return this.getBoolAttr('open'); }
|
|
104
116
|
set open(v) { this.setBoolAttr('open', v); }
|
|
105
117
|
}
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import { GlkFormElement } from './base.js';
|
|
2
|
+
import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
|
|
2
3
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
4
|
|
|
4
5
|
// One button per option, the chosen one marked aria-pressed="true" — GlassKit
|
|
5
6
|
// styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
|
|
6
7
|
//
|
|
7
8
|
// Attributes: options (JSON [{value,label,tone?,disabled?}]), value, full,
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// overflow ("scroll" | "wrap": what happens when the options
|
|
10
|
+
// do not fit — without it the row stays one line and runs past
|
|
11
|
+
// the edge), label (aria-label of the group), name (form field)
|
|
12
|
+
// Properties: value, options (array or JSON text), full, overflow, label
|
|
10
13
|
// Event: glk-change { value } — only on a change made by the user
|
|
11
14
|
// Part: group
|
|
12
15
|
//
|
|
13
16
|
// Form-associated: a surrounding <form> receives name=value, reset restores
|
|
14
17
|
// the initial value. A value change only re-sets aria-pressed, never rebuilds,
|
|
15
18
|
// so focus stays on the button that was pressed.
|
|
19
|
+
//
|
|
20
|
+
// overflow="scroll" keeps the chosen option in view: it is centred in the
|
|
21
|
+
// row after the first layout, on every value change and when the row's width
|
|
22
|
+
// changes, through the row's own scrolling, so the page never moves (since
|
|
23
|
+
// 1.17.0). The attribute is not called "scroll": that is a method of every
|
|
24
|
+
// element, and a framework that sets a property whenever the element has one
|
|
25
|
+
// (hybrids, Vue, React 19) would overwrite the method instead of setting the
|
|
26
|
+
// attribute.
|
|
16
27
|
|
|
17
28
|
const TONES = ['success', 'warning', 'error'];
|
|
29
|
+
const OVERFLOWS = ['scroll', 'wrap'];
|
|
18
30
|
|
|
19
31
|
function parseList(json) {
|
|
20
32
|
try {
|
|
@@ -26,14 +38,14 @@ function parseList(json) {
|
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
class GlkSegmented extends GlkFormElement {
|
|
29
|
-
static get observedAttributes() { return ['options', 'value', 'full', 'label']; }
|
|
41
|
+
static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
|
|
30
42
|
|
|
31
43
|
render() {
|
|
32
44
|
this._group = this.createElement('div', ['glass-segmented'], { role: 'group', part: 'group' });
|
|
33
45
|
this._wrapper.appendChild(this._group);
|
|
34
46
|
this._initialValue = this.value;
|
|
35
47
|
this._build();
|
|
36
|
-
this.
|
|
48
|
+
this._applyLayout();
|
|
37
49
|
this._applyLabel();
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -59,10 +71,20 @@ class GlkSegmented extends GlkFormElement {
|
|
|
59
71
|
button.setAttribute('aria-pressed', String(button.dataset.value === value));
|
|
60
72
|
}
|
|
61
73
|
this.setFormValue(value);
|
|
74
|
+
this._reveal(this._revealedAt ? 'smooth' : 'instant');
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
|
|
77
|
+
_applyLayout() {
|
|
78
|
+
const overflow = this.overflow;
|
|
65
79
|
this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
|
|
80
|
+
for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
|
|
81
|
+
this._reveal('instant');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_reveal(behavior) {
|
|
85
|
+
if (this.overflow !== 'scroll') return;
|
|
86
|
+
const pressed = this._group.querySelector('[aria-pressed="true"]');
|
|
87
|
+
if (revealCentered(this._group, pressed, behavior === 'smooth')) this._revealedAt = this._group.clientWidth;
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
_applyLabel() {
|
|
@@ -80,11 +102,16 @@ class GlkSegmented extends GlkFormElement {
|
|
|
80
102
|
this.value = value;
|
|
81
103
|
this.emit('glk-change', { value });
|
|
82
104
|
};
|
|
105
|
+
this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
|
|
83
106
|
this._group.addEventListener('click', this._onClick);
|
|
107
|
+
this._resizeObserver = new ResizeObserver(this._onResize);
|
|
108
|
+
this._resizeObserver.observe(this._group);
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
teardownEvents() {
|
|
87
112
|
this._group?.removeEventListener('click', this._onClick);
|
|
113
|
+
this._resizeObserver?.disconnect();
|
|
114
|
+
this._resizeObserver = null;
|
|
88
115
|
}
|
|
89
116
|
|
|
90
117
|
onAttributeChanged(name) {
|
|
@@ -92,7 +119,8 @@ class GlkSegmented extends GlkFormElement {
|
|
|
92
119
|
switch (name) {
|
|
93
120
|
case 'value': this._syncValue(); break;
|
|
94
121
|
case 'options': this._build(); break;
|
|
95
|
-
case 'full':
|
|
122
|
+
case 'full':
|
|
123
|
+
case 'overflow': this._applyLayout(); break;
|
|
96
124
|
case 'label': this._applyLabel(); break;
|
|
97
125
|
}
|
|
98
126
|
}
|
|
@@ -112,6 +140,15 @@ class GlkSegmented extends GlkFormElement {
|
|
|
112
140
|
get full() { return this.getBoolAttr('full'); }
|
|
113
141
|
set full(v) { this.setBoolAttr('full', v); }
|
|
114
142
|
|
|
143
|
+
get overflow() {
|
|
144
|
+
const v = this.getAttribute('overflow');
|
|
145
|
+
return OVERFLOWS.includes(v) ? v : '';
|
|
146
|
+
}
|
|
147
|
+
set overflow(v) {
|
|
148
|
+
if (v) this.setAttribute('overflow', String(v));
|
|
149
|
+
else this.removeAttribute('overflow');
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
get label() { return this.getAttribute('label') || ''; }
|
|
116
153
|
set label(v) {
|
|
117
154
|
if (v) this.setAttribute('label', v);
|
|
@@ -38,12 +38,14 @@ class GlkSheet extends GlkElement {
|
|
|
38
38
|
this._panel.append(this._titleEl, body, actions);
|
|
39
39
|
this._overlay.appendChild(this._panel);
|
|
40
40
|
this._wrapper.appendChild(this._overlay);
|
|
41
|
+
this.takeTitle();
|
|
41
42
|
this._applyTitle();
|
|
42
43
|
this._applyMode();
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
_applyTitle() {
|
|
46
|
-
|
|
47
|
+
if (!this._titleEl) return;
|
|
48
|
+
const title = this._title || '';
|
|
47
49
|
this._titleEl.textContent = title;
|
|
48
50
|
this._titleEl.hidden = !title;
|
|
49
51
|
if (title) this._panel.setAttribute('aria-label', title);
|
|
@@ -113,7 +115,7 @@ class GlkSheet extends GlkElement {
|
|
|
113
115
|
document.removeEventListener('keydown', this._onKeydown);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
onAttributeChanged(name) {
|
|
118
|
+
onAttributeChanged(name, _old, value) {
|
|
117
119
|
if (!this._overlay) return;
|
|
118
120
|
switch (name) {
|
|
119
121
|
case 'open':
|
|
@@ -122,13 +124,20 @@ class GlkSheet extends GlkElement {
|
|
|
122
124
|
else this._hide();
|
|
123
125
|
break;
|
|
124
126
|
case 'inline': this._applyMode(); break;
|
|
125
|
-
case 'title': this._applyTitle(); break;
|
|
127
|
+
case 'title': if (this.takeTitle(value)) this._applyTitle(); break;
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
show() { this.setAttribute('open', ''); }
|
|
130
132
|
close() { this.removeAttribute('open'); }
|
|
131
133
|
|
|
134
|
+
// `title` is the heading, never a tooltip — see GlkElement.takeTitle().
|
|
135
|
+
get title() { return this._title ?? ''; }
|
|
136
|
+
set title(v) {
|
|
137
|
+
this._title = v == null ? '' : String(v);
|
|
138
|
+
this._applyTitle();
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
get open() { return this.getBoolAttr('open'); }
|
|
133
142
|
set open(v) { this.setBoolAttr('open', v); }
|
|
134
143
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Scroll an item to the middle of its scrolling row — shared by
|
|
2
|
+
// <glk-date-strip> and <glk-segmented overflow="scroll">. Computed from
|
|
3
|
+
// client rects and applied to the row alone, where scrollIntoView would move
|
|
4
|
+
// every scrolling ancestor, the page included. A relative scrollBy works in
|
|
5
|
+
// both writing directions: scrollLeft runs negative in RTL, the distance on
|
|
6
|
+
// screen does not. Returns false while the row has no width — before the
|
|
7
|
+
// first layout, or hidden — so the caller can try again from a
|
|
8
|
+
// ResizeObserver. Pass smooth for a reveal that follows a value change, not
|
|
9
|
+
// for one caused by layout; reduced motion wins over it.
|
|
10
|
+
|
|
11
|
+
function revealCentered(scroller, item, smooth) {
|
|
12
|
+
if (!item || !scroller.clientWidth) return false;
|
|
13
|
+
const port = scroller.getBoundingClientRect();
|
|
14
|
+
const box = item.getBoundingClientRect();
|
|
15
|
+
const delta = box.left + box.width / 2 - (port.left + scroller.clientLeft + scroller.clientWidth / 2);
|
|
16
|
+
const glide = smooth && !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
17
|
+
scroller.scrollBy({ left: delta, behavior: glide ? 'smooth' : 'instant' });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { revealCentered as r };
|