@jungherz-de/glasskit 1.30.0 → 1.32.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.
@@ -25,6 +25,13 @@
25
25
  // and data-droplet-lift while the drop travels, data-droplet-drag while a
26
26
  // finger holds it, and .is-under-droplet on the item below it.
27
27
  //
28
+ // A row laid out as a column — a vertical tab bar, a sidebar — runs the drop
29
+ // along y (since 1.32.0): data-droplet-axis="y" on the row, the directions
30
+ // "down" and "up", the drag on the vertical. The axis comes from the row's
31
+ // aria-orientation="vertical" or its flex-direction. The timing comes from
32
+ // the --gl-droplet-* tokens, which the module reads as well: the lift and how
33
+ // long the lens follows the drop.
34
+ //
28
35
  // Options of attachDroplet, for rows whose items live elsewhere (GlassKit
29
36
  // Elements uses them): items() returns the items, isChosen(item) tells the
30
37
  // chosen one, choose(item) picks one, lens(item, on) marks the item below
@@ -36,6 +43,13 @@ const ROWS = '.glass-tab-bar--droplet, .glass-segmented--droplet';
36
43
  const ITEMS = '.glass-tab-bar__item, .glass-segmented__item';
37
44
  const attached = new WeakMap();
38
45
 
46
+ // A duration token as the browser hands it over, "0.3s" or "280ms".
47
+ const ms = (value, fallback) => {
48
+ const v = String(value).trim(), n = parseFloat(v);
49
+ if (Number.isNaN(n)) return fallback;
50
+ return v.endsWith('ms') ? n : v.endsWith('s') ? n * 1000 : fallback;
51
+ };
52
+
39
53
  export function attachDroplet(row, options = {}) {
40
54
  if (attached.has(row)) return attached.get(row);
41
55
 
@@ -45,12 +59,17 @@ export function attachDroplet(row, options = {}) {
45
59
  const choose = options.choose || (it => it.click());
46
60
  const mark = options.lens || ((it, on) => it.classList.toggle('is-under-droplet', on));
47
61
  const usable = it => !it.disabled && it.getAttribute('aria-disabled') !== 'true';
48
- const scrolls = () => row.scrollWidth > row.clientWidth + 1;
49
62
  const motion = matchMedia('(prefers-reduced-motion: no-preference)');
50
63
  const set = (k, px) => row.style.setProperty(`--gl-droplet-${k}`, `${px}px`);
64
+ const token = (name, fallback) => ms(getComputedStyle(row).getPropertyValue(`--gl-droplet-${name}`), fallback);
51
65
 
52
- let chosen = null, left = 0, placed = null, liftTimer = 0, lensFrame = 0, edgeFrame = 0;
53
- let drag = null, eatClick = false, under = null;
66
+ // pos is where the drop starts along the axis: left in a row, top in a
67
+ // column. The axis is read again whenever the drop is placed.
68
+ let chosen = null, pos = 0, placed = null, liftTimer = 0, lensFrame = 0, edgeFrame = 0;
69
+ let drag = null, eatClick = false, under = null, axisY = false;
70
+ const vertical = () => row.getAttribute('aria-orientation') === 'vertical'
71
+ || getComputedStyle(row).flexDirection.startsWith('column');
72
+ const scrolls = () => axisY ? row.scrollHeight > row.clientHeight + 1 : row.scrollWidth > row.clientWidth + 1;
54
73
 
55
74
  // The row on screen and its scale there. Rects are viewport pixels, scaled
56
75
  // by every transform on an ancestor — an app that zooms in on start, a
@@ -77,13 +96,14 @@ export function attachDroplet(row, options = {}) {
77
96
  const l = (r.left - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
78
97
  const t = (r.top - f.n.top) / f.sy - row.clientTop + row.scrollTop;
79
98
  const height = r.height / f.sy;
80
- return { left: l, width: r.width / f.sx, top: t, bottom: row.clientHeight - t - height };
99
+ return { left: l, width: r.width / f.sx, top: t, height, bottom: row.clientHeight - t - height };
81
100
  };
82
- const put = (l, width, b) => {
83
- left = l;
84
- set('left', l); set('right', row.clientWidth - l - width);
85
- set('top', b.top); set('bottom', b.bottom);
101
+ const put = b => {
102
+ pos = axisY ? b.top : b.left;
103
+ set('left', b.left); set('right', row.clientWidth - b.left - b.width);
104
+ set('top', b.top); set('bottom', row.clientHeight - b.top - b.height);
86
105
  };
106
+ const centreOf = b => axisY ? b.top + b.height / 2 : b.left + b.width / 2;
87
107
 
88
108
  // The usable item nearest a point of the row lights up as under a lens.
89
109
  const lens = centre => {
@@ -91,7 +111,7 @@ export function attachDroplet(row, options = {}) {
91
111
  const f = centre == null ? null : frame();
92
112
  if (f) for (const it of items()) {
93
113
  if (!usable(it)) continue;
94
- const b = boxOf(it, f), d = Math.abs(b.left + b.width / 2 - centre);
114
+ const d = Math.abs(centreOf(boxOf(it, f)) - centre);
95
115
  if (d < best) { best = d; below = it; }
96
116
  }
97
117
  if (below !== under) {
@@ -101,33 +121,43 @@ export function attachDroplet(row, options = {}) {
101
121
  }
102
122
  return below;
103
123
  };
104
- // While the drop travels, the lens follows its centre.
124
+ // While the drop travels, the lens follows its centre — as long as its
125
+ // edges travel by the tokens, the trailing one the longer (620 ms unset).
105
126
  const follow = () => {
106
127
  cancelAnimationFrame(lensFrame);
107
128
  const start = performance.now();
129
+ const length = Math.max(token('duration', 300), token('trail-delay', 40) + token('trail-duration', 500)) + 80;
108
130
  const step = now => {
109
- if (now - start > 620) { lens(null); return; }
131
+ if (now - start > length) { lens(null); return; }
110
132
  const cs = getComputedStyle(row, '::before');
111
- const l = parseFloat(cs.left), r = parseFloat(cs.right);
112
- lens(l + (row.clientWidth - l - r) / 2);
133
+ if (axisY) {
134
+ const t = parseFloat(cs.top), b = parseFloat(cs.bottom);
135
+ lens(t + (row.clientHeight - t - b) / 2);
136
+ } else {
137
+ const l = parseFloat(cs.left), r = parseFloat(cs.right);
138
+ lens(l + (row.clientWidth - l - r) / 2);
139
+ }
113
140
  lensFrame = requestAnimationFrame(step);
114
141
  };
115
142
  lensFrame = requestAnimationFrame(step);
116
143
  };
117
144
 
118
145
  const place = (it, travel) => {
119
- const b = boxOf(it);
146
+ axisY = vertical();
147
+ if (axisY) row.dataset.dropletAxis = 'y';
148
+ else delete row.dataset.dropletAxis;
149
+ const b = boxOf(it), to = axisY ? b.top : b.left;
120
150
  row.dataset.droplet = '';
121
- if (travel && motion.matches && Math.abs(b.left - left) > 1) {
122
- row.dataset.dropletDir = b.left > left ? 'right' : 'left';
151
+ if (travel && motion.matches && Math.abs(to - pos) > 1) {
152
+ row.dataset.dropletDir = axisY ? (to > pos ? 'down' : 'up') : (to > pos ? 'right' : 'left');
123
153
  row.dataset.dropletLift = '';
124
154
  clearTimeout(liftTimer);
125
- liftTimer = setTimeout(() => { delete row.dataset.dropletLift; }, 280);
155
+ liftTimer = setTimeout(() => { delete row.dataset.dropletLift; }, token('lift-duration', 280));
126
156
  follow();
127
157
  } else {
128
158
  delete row.dataset.dropletDir;
129
159
  }
130
- put(b.left, b.width, b);
160
+ put(b);
131
161
  placed = { ...b, rowWidth: row.clientWidth, rowHeight: row.clientHeight };
132
162
  chosen = it;
133
163
  };
@@ -155,34 +185,42 @@ export function attachDroplet(row, options = {}) {
155
185
 
156
186
  // Dragging: past 8px the drop slides under the finger and grows into a
157
187
  // lens; let go, the item below it is chosen and the drop settles on it.
158
- const at = clientX => {
188
+ // Along the row's axis: x in a row, y in a column.
189
+ const at = client => {
159
190
  const f = frame();
160
- const x = (clientX - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
191
+ const p = axisY
192
+ ? (client - f.n.top) / f.sy - row.clientTop + row.scrollTop
193
+ : (client - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
161
194
  const boxes = items().map(it => boxOf(it, f));
162
- const min = Math.min(...boxes.map(b => b.left));
163
- const max = Math.max(...boxes.map(b => b.left + b.width)) - drag.width;
164
- return Math.min(max, Math.max(min, x - drag.width / 2));
195
+ const min = Math.min(...boxes.map(b => axisY ? b.top : b.left));
196
+ const max = Math.max(...boxes.map(b => axisY ? b.top + b.height : b.left + b.width)) - drag.size;
197
+ return Math.min(max, Math.max(min, p - drag.size / 2));
165
198
  };
166
199
  const track = () => {
167
- const l = at(drag.clientX);
168
- put(l, drag.width, boxOf(chosen));
169
- drag.below = lens(l + drag.width / 2);
200
+ const p = at(drag.client), b = boxOf(chosen);
201
+ put(axisY ? { ...b, top: p, height: drag.size } : { ...b, left: p, width: drag.size });
202
+ drag.below = lens(p + drag.size / 2);
170
203
  cancelAnimationFrame(edgeFrame);
171
204
  if (!scrolls()) return;
172
205
  const n = row.getBoundingClientRect(), zone = 32;
173
- const v = drag.clientX < n.left + zone ? -1 : drag.clientX > n.right - zone ? 1 : 0;
174
- if (v) edgeFrame = requestAnimationFrame(() => { row.scrollLeft += v * 6; if (drag?.moved) track(); });
206
+ const lo = axisY ? n.top : n.left, hi = axisY ? n.bottom : n.right;
207
+ const v = drag.client < lo + zone ? -1 : drag.client > hi - zone ? 1 : 0;
208
+ if (v) edgeFrame = requestAnimationFrame(() => {
209
+ if (axisY) row.scrollTop += v * 6; else row.scrollLeft += v * 6;
210
+ if (drag?.moved) track();
211
+ });
175
212
  };
176
213
  const down = e => {
177
214
  if (!chosen || drag || !usable(chosen) || (e.pointerType === 'mouse' && e.button !== 0)) return;
178
215
  if (scrolls() && !e.composedPath().includes(chosen)) return;
179
- drag = { id: e.pointerId, x: e.clientX, clientX: e.clientX, width: boxOf(chosen).width, moved: false, below: null };
216
+ const b = boxOf(chosen), client = axisY ? e.clientY : e.clientX;
217
+ drag = { id: e.pointerId, start: client, client, size: axisY ? b.height : b.width, moved: false, below: null };
180
218
  };
181
219
  const move = e => {
182
220
  if (!drag || e.pointerId !== drag.id) return;
183
- drag.clientX = e.clientX;
221
+ drag.client = axisY ? e.clientY : e.clientX;
184
222
  if (!drag.moved) {
185
- if (Math.abs(e.clientX - drag.x) < 8) return;
223
+ if (Math.abs(drag.client - drag.start) < 8) return;
186
224
  drag.moved = true;
187
225
  row.setPointerCapture(e.pointerId);
188
226
  delete row.dataset.dropletDir;
@@ -226,7 +264,7 @@ export function attachDroplet(row, options = {}) {
226
264
  row.removeEventListener('pointercancel', up);
227
265
  row.removeEventListener('click', click, true);
228
266
  lens(null);
229
- for (const k of ['droplet', 'dropletDir', 'dropletLift', 'dropletDrag']) delete row.dataset[k];
267
+ for (const k of ['droplet', 'dropletDir', 'dropletLift', 'dropletDrag', 'dropletAxis']) delete row.dataset[k];
230
268
  for (const k of ['left', 'right', 'top', 'bottom']) row.style.removeProperty(`--gl-droplet-${k}`);
231
269
  attached.delete(row);
232
270
  },