@jungherz-de/glasskit-elements 1.25.0 → 1.27.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.25.0",
3
+ "version": "1.27.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.25.0"
52
+ "@jungherz-de/glasskit": ">=1.27.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@jungherz-de/glasskit": "^1.25.0",
55
+ "@jungherz-de/glasskit": "^1.27.0",
56
56
  "@rollup/plugin-node-resolve": "^15.0.0",
57
57
  "@rollup/plugin-terser": "^0.4.0",
58
58
  "rollup": "^4.0.0"
@@ -1,5 +1,6 @@
1
1
  import { GlkFormElement } from '../../base.js';
2
2
  import { revealCentered } from '../../reveal.js';
3
+ import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.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.
@@ -9,9 +10,11 @@ import { revealCentered } from '../../reveal.js';
9
10
  // do not fit — without it the row stays one line and runs past
10
11
  // the edge), label (aria-label of the group), name (form field),
11
12
  // disabled (the whole row; since 1.22.3, as is a disabled
12
- // fieldset around it)
13
+ // fieldset around it), droplet (since 1.27.0: the chosen option
14
+ // sits on a drop of glass that glides to the next one and can be
15
+ // dragged — GlassKit's .glass-segmented--droplet)
13
16
  // Properties: value, options (array or JSON text), full, overflow, label,
14
- // disabled
17
+ // disabled, droplet
15
18
  // Event: glk-change { value } — only on a change made by the user
16
19
  // Part: group
17
20
  //
@@ -40,7 +43,7 @@ function parseList(json) {
40
43
  }
41
44
 
42
45
  class GlkSegmented extends GlkFormElement {
43
- static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
46
+ static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet']; }
44
47
 
45
48
  // A row of buttons with one tab stop: delegated focus would land on the
46
49
  // first button, not the chosen one.
@@ -91,6 +94,7 @@ class GlkSegmented extends GlkFormElement {
91
94
  _applyLayout() {
92
95
  const overflow = this.overflow;
93
96
  this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
97
+ this._group.classList.toggle('glass-segmented--droplet', this.getBoolAttr('droplet'));
94
98
  for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
95
99
  this._reveal('instant');
96
100
  }
@@ -120,12 +124,23 @@ class GlkSegmented extends GlkFormElement {
120
124
  this._group.addEventListener('click', this._onClick);
121
125
  this._resizeObserver = new ResizeObserver(this._onResize);
122
126
  this._resizeObserver.observe(this._group);
127
+ this._applyDroplet();
123
128
  }
124
129
 
125
130
  teardownEvents() {
126
131
  this._group?.removeEventListener('click', this._onClick);
127
132
  this._resizeObserver?.disconnect();
128
133
  this._resizeObserver = null;
134
+ this._droplet?.destroy();
135
+ this._droplet = null;
136
+ }
137
+
138
+ // The drop follows aria-pressed inside this shadow root by itself; a drag
139
+ // that ends on an option clicks it, the same path as a tap.
140
+ _applyDroplet() {
141
+ const on = this.getBoolAttr('droplet') && this.isConnected;
142
+ if (on && !this._droplet) this._droplet = attachDroplet(this._group);
143
+ else if (!on && this._droplet) { this._droplet.destroy(); this._droplet = null; }
129
144
  }
130
145
 
131
146
  onAttributeChanged(name) {
@@ -135,6 +150,7 @@ class GlkSegmented extends GlkFormElement {
135
150
  case 'options': this._build(); break;
136
151
  case 'full':
137
152
  case 'overflow': this._applyLayout(); break;
153
+ case 'droplet': this._applyLayout(); this._applyDroplet(); break;
138
154
  case 'label': this._applyLabel(); break;
139
155
  }
140
156
  }
@@ -154,6 +170,9 @@ class GlkSegmented extends GlkFormElement {
154
170
  get full() { return this.getBoolAttr('full'); }
155
171
  set full(v) { this.setBoolAttr('full', v); }
156
172
 
173
+ get droplet() { return this.getBoolAttr('droplet'); }
174
+ set droplet(v) { this.setBoolAttr('droplet', v); }
175
+
157
176
  get overflow() {
158
177
  const v = this.getAttribute('overflow');
159
178
  return OVERFLOWS.includes(v) ? v : '';
@@ -1,8 +1,15 @@
1
1
  import { GlkElement } from '../../base.js';
2
+ import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
3
+
4
+ // droplet (since 1.27.0): the active item sits on a drop of glass that glides
5
+ // to the next one and can be dragged — GlassKit's .glass-tab-bar--droplet,
6
+ // moved by its glasskit-droplet.js. The items live in shadow roots of their
7
+ // own, so the bar hands its state down to them (see glk-tab-item _syncBar)
8
+ // and tells the drop when the active item changed.
2
9
 
3
10
  class GlkTabBar extends GlkElement {
4
11
  static get observedAttributes() {
5
- return ['static', 'floating'];
12
+ return ['static', 'floating', 'droplet'];
6
13
  }
7
14
 
8
15
  render() {
@@ -14,6 +21,7 @@ class GlkTabBar extends GlkElement {
14
21
  if (this.getBoolAttr('floating')) {
15
22
  nav.classList.add('glass-tab-bar--floating');
16
23
  }
24
+ nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
17
25
 
18
26
  if (this.getBoolAttr('static')) {
19
27
  this._applyStatic();
@@ -26,6 +34,10 @@ class GlkTabBar extends GlkElement {
26
34
  this._applyStatic();
27
35
  } else if (name === 'floating') {
28
36
  this._nav.classList.toggle('glass-tab-bar--floating', this.getBoolAttr('floating'));
37
+ this._syncItems();
38
+ } else if (name === 'droplet') {
39
+ this._nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
40
+ this._applyDroplet();
29
41
  }
30
42
  }
31
43
 
@@ -34,6 +46,42 @@ class GlkTabBar extends GlkElement {
34
46
  this._nav.style.borderRadius = '16px';
35
47
  }
36
48
 
49
+ _items() {
50
+ return [...this.children].filter(el => el.localName === 'glk-tab-item');
51
+ }
52
+
53
+ /** Hands the bar's floating and droplet state to every item's wrapper. */
54
+ _syncItems() {
55
+ const state = {
56
+ floating: this.getBoolAttr('floating'),
57
+ droplet: this.getBoolAttr('droplet'),
58
+ placed: Boolean(this._nav?.hasAttribute('data-droplet')),
59
+ drag: Boolean(this._nav?.hasAttribute('data-droplet-drag')),
60
+ };
61
+ for (const item of this._items()) item._syncBar?.(state);
62
+ }
63
+
64
+ /** Called by an item that rendered or changed its active state. */
65
+ _itemChanged() {
66
+ this._syncItems();
67
+ this._droplet?.update();
68
+ }
69
+
70
+ _applyDroplet() {
71
+ const on = this.getBoolAttr('droplet') && this.isConnected;
72
+ if (on && !this._droplet) {
73
+ // The drop measures and marks the items' own buttons; picking one clicks
74
+ // it, which is what a tap does.
75
+ this._droplet = attachDroplet(this._nav, {
76
+ items: () => this._items().map(item => item._btn).filter(Boolean),
77
+ });
78
+ } else if (!on && this._droplet) {
79
+ this._droplet.destroy();
80
+ this._droplet = null;
81
+ }
82
+ this._syncItems();
83
+ }
84
+
37
85
  setupEvents() {
38
86
  // Listen for tab selection events from child glk-tab-item elements
39
87
  this._onTabSelect = (e) => {
@@ -47,10 +95,22 @@ class GlkTabBar extends GlkElement {
47
95
  this.emit('glk-tab-change', { tab: e.target });
48
96
  };
49
97
  this.addEventListener('glk-tab-select', this._onTabSelect);
98
+
99
+ // The drop's state lives on the nav; the items carry it on their wrappers.
100
+ this._navObserver = new MutationObserver(() => this._syncItems());
101
+ this._navObserver.observe(this._nav, { attributes: true, attributeFilter: ['data-droplet', 'data-droplet-drag'] });
102
+ // Items added or removed.
103
+ this._itemObserver = new MutationObserver(() => this._itemChanged());
104
+ this._itemObserver.observe(this, { childList: true });
105
+ this._applyDroplet();
50
106
  }
51
107
 
52
108
  teardownEvents() {
53
109
  this.removeEventListener('glk-tab-select', this._onTabSelect);
110
+ this._navObserver?.disconnect();
111
+ this._itemObserver?.disconnect();
112
+ this._droplet?.destroy();
113
+ this._droplet = null;
54
114
  }
55
115
  }
56
116
 
@@ -66,6 +66,27 @@ class GlkTabItem extends GlkElement {
66
66
  this.emit('glk-tab-select');
67
67
  };
68
68
  this._btn.addEventListener('click', this._onClick);
69
+ this._tellBar();
70
+ }
71
+
72
+ /**
73
+ * The bar's look reaches the item through its wrapper. GlassKit's rules for
74
+ * floating and droplet items are descendant selectors of the bar's classes,
75
+ * and this button sits in a shadow root of its own, so <glk-tab-bar> hands
76
+ * its state down and the wrapper carries it. Before 1.27.0 the items of a
77
+ * floating bar kept the fixed bar's look: no spotlight, no pill, the dot.
78
+ */
79
+ _syncBar({ floating, droplet, placed, drag }) {
80
+ if (!this._wrapper) return;
81
+ this._wrapper.classList.toggle('glass-tab-bar--floating', floating);
82
+ this._wrapper.classList.toggle('glass-tab-bar--droplet', droplet);
83
+ this._wrapper.toggleAttribute('data-droplet', placed);
84
+ this._wrapper.toggleAttribute('data-droplet-drag', drag);
85
+ }
86
+
87
+ /** Tells the bar that this item rendered or changed its active state. */
88
+ _tellBar() {
89
+ this.closest('glk-tab-bar')?._itemChanged?.();
69
90
  }
70
91
 
71
92
  teardownEvents() {
@@ -77,6 +98,7 @@ class GlkTabItem extends GlkElement {
77
98
  switch (name) {
78
99
  case 'active':
79
100
  this._btn.classList.toggle('is-active', this.getBoolAttr('active'));
101
+ this._tellBar();
80
102
  break;
81
103
  case 'label':
82
104
  this._labelEl.textContent = this.getAttribute('label') || '';