@jungherz-de/glasskit-elements 1.28.0 → 1.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungherz-de/glasskit-elements",
3
- "version": "1.28.0",
3
+ "version": "1.29.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.28.0"
52
+ "@jungherz-de/glasskit": ">=1.29.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@jungherz-de/glasskit": "^1.28.0",
55
+ "@jungherz-de/glasskit": "^1.29.0",
56
56
  "@rollup/plugin-node-resolve": "^15.0.0",
57
57
  "@rollup/plugin-terser": "^0.4.0",
58
58
  "rollup": "^4.0.0"
@@ -5,7 +5,11 @@ import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
5
5
  // One button per option, the chosen one marked aria-pressed="true" — GlassKit
6
6
  // styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
7
7
  //
8
- // Attributes: options (JSON [{value,label,tone?,disabled?}]), value, full,
8
+ // Attributes: options (JSON [{value,label,tone?,disabled?,badge?,badgeLabel?}]
9
+ // — badge since 1.29.0: a count after the label, GlassKit's
10
+ // .glass-segmented__badge, in the tone's colour where one is
11
+ // set; missing, empty or 0 shows none; badgeLabel words it for
12
+ // the name, "1 open"), value, full,
9
13
  // overflow ("scroll" | "wrap": what happens when the options
10
14
  // do not fit — without it the row stays one line and runs past
11
15
  // the edge), label (aria-label of the group), name (form field),
@@ -27,7 +31,9 @@ import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
27
31
  //
28
32
  // Form-associated: a surrounding <form> receives name=value, reset restores
29
33
  // the initial value. A value change only re-sets aria-pressed, never rebuilds,
30
- // so focus stays on the button that was pressed.
34
+ // so focus stays on the button that was pressed. New options with the same
35
+ // values in the same order — a count that changed — fill the buttons in
36
+ // place (since 1.29.0), so the focus and the drop stay as well.
31
37
  //
32
38
  // overflow="scroll" keeps the chosen option in view: it is centred in the
33
39
  // row after the first layout, on every value change and when the row's width
@@ -49,6 +55,13 @@ function parseList(json) {
49
55
  }
50
56
  }
51
57
 
58
+ /** The text of an option's count; empty when there is none to show. */
59
+ function badgeText(badge) {
60
+ if (badge == null || badge === false) return '';
61
+ const text = String(badge).trim();
62
+ return text === '0' ? '' : text;
63
+ }
64
+
52
65
  class GlkSegmented extends GlkFormElement {
53
66
  static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet', 'icon-only']; }
54
67
 
@@ -66,33 +79,54 @@ class GlkSegmented extends GlkFormElement {
66
79
  }
67
80
 
68
81
  _build() {
69
- this._group.replaceChildren();
82
+ const list = parseList(this.getAttribute('options'));
70
83
  const iconOnly = this.iconOnly;
71
- for (const option of parseList(this.getAttribute('options'))) {
72
- const value = String(option.value ?? '');
73
- const label = String(option.label ?? value);
74
- const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
75
- if (TONES.includes(option.tone)) {
76
- button.classList.add(`glass-segmented__item--${option.tone}`);
77
- button.appendChild(this.createElement('span', ['glass-segmented__dot']));
78
- }
79
- // The label names the button; an icon font's text must not join it.
80
- const icon = this.createElement('span', ['glass-segmented__icon'], { 'aria-hidden': 'true' });
81
- icon.appendChild(this.createElement('slot', [], { name: `icon-${value}` }));
82
- button.appendChild(icon);
83
- if (iconOnly) {
84
- button.setAttribute('aria-label', label);
85
- button.title = label;
86
- } else {
87
- button.appendChild(document.createTextNode(label));
88
- }
89
- this._group.appendChild(button);
90
- }
84
+ // The same values in the same order keep their buttons; anything else
85
+ // starts the row afresh.
86
+ const buttons = [...this._group.children];
87
+ const same = buttons.length === list.length
88
+ && list.every((option, i) => buttons[i].dataset.value === String(option.value ?? ''));
89
+ if (!same) this._group.replaceChildren(...list.map(() => this.createElement('button', [], { type: 'button' })));
90
+ [...this._group.children].forEach((button, i) => this._fill(button, list[i], iconOnly));
91
91
  this._syncIcons();
92
92
  this.applyDisabled(this._actuallyDisabled);
93
93
  this._syncValue();
94
94
  }
95
95
 
96
+ /** Gives an option's button its tone, icon box, label and count. */
97
+ _fill(button, option, iconOnly) {
98
+ const value = String(option.value ?? '');
99
+ const label = String(option.label ?? value);
100
+ const parts = [];
101
+ button.className = 'glass-segmented__item';
102
+ button.dataset.value = value;
103
+ if (TONES.includes(option.tone)) {
104
+ button.classList.add(`glass-segmented__item--${option.tone}`);
105
+ parts.push(this.createElement('span', ['glass-segmented__dot']));
106
+ }
107
+ // The label names the button; an icon font's text must not join it.
108
+ const icon = this.createElement('span', ['glass-segmented__icon'], { 'aria-hidden': 'true' });
109
+ icon.appendChild(this.createElement('slot', [], { name: `icon-${value}` }));
110
+ parts.push(icon);
111
+ if (!iconOnly) parts.push(document.createTextNode(label));
112
+ // The count after the label, or at the icon's corner in an icon-only row;
113
+ // the name says it in words — "Tickets, 1 open".
114
+ const badge = badgeText(option.badge);
115
+ let name = label;
116
+ if (badge) {
117
+ const pill = this.createElement('span', ['glass-segmented__badge']);
118
+ pill.textContent = badge;
119
+ if (iconOnly) icon.appendChild(pill);
120
+ else parts.push(pill);
121
+ name = `${label}, ${option.badgeLabel ? String(option.badgeLabel) : badge}`;
122
+ }
123
+ button.replaceChildren(...parts);
124
+ if (iconOnly || badge) button.setAttribute('aria-label', name);
125
+ else button.removeAttribute('aria-label');
126
+ if (iconOnly) button.title = name;
127
+ else button.removeAttribute('title');
128
+ }
129
+
96
130
  /** An option shows its icon box only while an icon is slotted into it. */
97
131
  _syncIcons() {
98
132
  for (const slot of this._group.querySelectorAll('.glass-segmented__icon > slot')) {
@@ -140,7 +174,10 @@ class GlkSegmented extends GlkFormElement {
140
174
 
141
175
  setupEvents() {
142
176
  this._onClick = (event) => {
143
- const button = event.target.closest('button[data-value]');
177
+ // The option's button from the event's path. A click on a slotted icon
178
+ // has that icon as its target, in the light DOM, where closest() never
179
+ // meets the button — until 1.28.1 such a tap chose nothing.
180
+ const button = event.composedPath().find(node => node.parentNode === this._group);
144
181
  if (!button || button.disabled) return;
145
182
  const { value } = button.dataset;
146
183
  if (value === this.value) return;