ember-primitives 0.61.0 → 0.62.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.
@@ -1,6 +1,7 @@
1
1
  const GROUP_SELECTOR = '.ember-primitives__resizable';
2
- const PANEL_SELECTOR = '.ember-primitives__resizable__panel';
3
- const HANDLE_SELECTOR = '.ember-primitives__resizable__handle';
2
+ const PANEL_CLASS = 'ember-primitives__resizable__panel';
3
+ const HANDLE_CLASS = 'ember-primitives__resizable__handle';
4
+ const MEMBER_SELECTOR = `.${PANEL_CLASS}, .${HANDLE_CLASS}`;
4
5
  const DEFAULT_MIN = 0;
5
6
  const DEFAULT_MAX = 100;
6
7
 
@@ -40,6 +41,44 @@ function isCollapsible(panel) {
40
41
  function sameMembers(a, b) {
41
42
  return a.length === b.length && a.every((element, index) => element === b[index]);
42
43
  }
44
+ function precedes(a, b) {
45
+ return Boolean(b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_PRECEDING);
46
+ }
47
+
48
+ /**
49
+ * The `data-collapsed` attribute is the source of truth for collapse
50
+ * state (it is also the styling hook consumers use).
51
+ */
52
+ function isCollapsed(panel) {
53
+ return isCollapsible(panel) && panel.hasAttribute('data-collapsed');
54
+ }
55
+ function setCollapsed(panel, collapsed) {
56
+ if (collapsed === panel.hasAttribute('data-collapsed')) return;
57
+ if (collapsed) {
58
+ panel.setAttribute('data-collapsed', '');
59
+ } else {
60
+ panel.removeAttribute('data-collapsed');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * The panels immediately before and after the given handle element,
66
+ * in document order.
67
+ */
68
+ function neighborsOf(handleElement, panels) {
69
+ let prev = null;
70
+ let next = null;
71
+ for (const panel of panels) {
72
+ const position = handleElement.compareDocumentPosition(panel);
73
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) {
74
+ prev = panel;
75
+ } else if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
76
+ next = panel;
77
+ break;
78
+ }
79
+ }
80
+ return [prev, next];
81
+ }
43
82
 
44
83
  /**
45
84
  * Sizes are floats that get re-derived (measurement, normalization),
@@ -119,16 +158,27 @@ class GroupState {
119
158
  * Panels of nested groups belong to their own group, not this one.
120
159
  */
121
160
  get panels() {
122
- return this.#query(PANEL_SELECTOR);
161
+ return this.#members().panels;
123
162
  }
124
163
  get handles() {
125
- return this.#query(HANDLE_SELECTOR);
164
+ return this.#members().handles;
126
165
  }
127
- #query(selector) {
166
+ #members() {
167
+ const panels = [];
168
+ const handles = [];
128
169
  const element = this.element;
129
- if (!element) return [];
130
- const all = Array.from(element.querySelectorAll(selector));
131
- return all.filter(candidate => candidate.closest(GROUP_SELECTOR) === element);
170
+ if (!element) return {
171
+ panels,
172
+ handles
173
+ };
174
+ for (const member of element.querySelectorAll(MEMBER_SELECTOR)) {
175
+ if (member.closest(GROUP_SELECTOR) !== element) continue;
176
+ (member.classList.contains(PANEL_CLASS) ? panels : handles).push(member);
177
+ }
178
+ return {
179
+ panels,
180
+ handles
181
+ };
132
182
  }
133
183
  get sizes() {
134
184
  return this.panels.map(panel => this.#sizes.get(panel) ?? 0);
@@ -175,7 +225,8 @@ class GroupState {
175
225
  if (group && !affected.has(group)) affected.set(group, false);
176
226
  }
177
227
  for (const [group, orientationChanged] of affected) {
178
- if (orientationChanged || group.#membershipChanged()) group.#layout();
228
+ const members = group.#members();
229
+ if (orientationChanged || group.#membershipChanged(members)) group.#layout(members);
179
230
  }
180
231
  }
181
232
  static #observe(element, group) {
@@ -216,8 +267,8 @@ class GroupState {
216
267
  this.element = null;
217
268
  };
218
269
  };
219
- #membershipChanged() {
220
- return !sameMembers(this.panels, this.#knownPanels) || !sameMembers(this.handles, this.#knownHandles);
270
+ #membershipChanged(members) {
271
+ return !sameMembers(members.panels, this.#knownPanels) || !sameMembers(members.handles, this.#knownHandles);
221
272
  }
222
273
 
223
274
  /**
@@ -225,17 +276,21 @@ class GroupState {
225
276
  * keep it; new panels take a share of the remaining space; everything
226
277
  * is normalized to 100.
227
278
  */
228
- #layout() {
229
- const panels = this.panels;
230
- const handles = this.handles;
231
- if (!sameMembers(panels, this.#knownPanels)) this.#knownPanels = panels;
232
- if (!sameMembers(handles, this.#knownHandles)) this.#knownHandles = handles;
279
+ #layout(members = this.#members()) {
280
+ const {
281
+ panels,
282
+ handles
283
+ } = members;
284
+ if (this.#drag) this.#drag.members = members;
285
+ this.#knownPanels = panels;
286
+ this.#knownHandles = handles;
233
287
  if (panels.length === 0) return;
234
288
  let changed = false;
235
289
 
236
290
  // forget sizes of panels that left the DOM
237
- for (const known of Array.from(this.#sizes.keys())) {
238
- if (!panels.includes(known)) {
291
+ const current = new Set(panels);
292
+ for (const known of this.#sizes.keys()) {
293
+ if (!current.has(known)) {
239
294
  this.#sizes.delete(known);
240
295
  changed = true;
241
296
  }
@@ -252,7 +307,7 @@ class GroupState {
252
307
  unspecified.push(panel);
253
308
  continue;
254
309
  }
255
- const size = this.#isCollapsed(panel) ? preferred : clamp(preferred, minSizeOf(panel), maxSizeOf(panel));
310
+ const size = isCollapsed(panel) ? preferred : clamp(preferred, minSizeOf(panel), maxSizeOf(panel));
256
311
  computed.set(panel, size);
257
312
  specifiedTotal += size;
258
313
  }
@@ -302,8 +357,8 @@ class GroupState {
302
357
  changed = true;
303
358
  }
304
359
  }
305
- this.#apply(changedPanels);
306
- if (changed) this.#notify();
360
+ this.#apply(changedPanels, members);
361
+ if (changed) this.#notify(panels);
307
362
  }
308
363
 
309
364
  /**
@@ -314,24 +369,22 @@ class GroupState {
314
369
  * points, not derived from sizes -- rendered pixel sizes include
315
370
  * borders/padding, so a collapsed panel rarely measures exactly 0.)
316
371
  */
317
- #apply(candidates) {
318
- if (candidates.length > 0) {
319
- /**
320
- * With no unit fixed yet, nothing has ever been written, so every
321
- * panel renders at the CSS default -- an equal 1/n share.
322
- */
323
- const unit = this.#unit ?? 100 / this.panels.length;
324
- for (const panel of candidates) {
325
- const size = this.#sizes.get(panel);
326
- if (size === undefined) continue;
327
- const inlineGrow = panel.style.flexGrow;
328
- const impliedPercent = (inlineGrow === '' ? 1 : parseFloat(inlineGrow)) * unit;
329
- if (isSameSize(impliedPercent, size)) continue;
330
- this.#unit ??= unit;
331
- panel.style.flex = `${size / unit} 1 0px`;
332
- }
372
+ #apply(candidates, members) {
373
+ /**
374
+ * With no unit fixed yet, nothing has ever been written, so every
375
+ * panel renders at the CSS default -- an equal 1/n share.
376
+ */
377
+ const unit = this.#unit ?? 100 / members.panels.length;
378
+ for (const panel of candidates) {
379
+ const size = this.#sizes.get(panel);
380
+ if (size === undefined) continue;
381
+ const inlineGrow = panel.style.flexGrow;
382
+ const impliedPercent = (inlineGrow === '' ? 1 : parseFloat(inlineGrow)) * unit;
383
+ if (isSameSize(impliedPercent, size)) continue;
384
+ this.#unit ??= unit;
385
+ panel.style.flex = `${size / unit} 1 0px`;
333
386
  }
334
- this.#syncHandles();
387
+ this.#syncHandles(members);
335
388
  }
336
389
 
337
390
  /**
@@ -339,10 +392,21 @@ class GroupState {
339
392
  * immediately before it. (A splitter between two side-by-side panes
340
393
  * is oriented *vertically*, and vice-versa.)
341
394
  */
342
- #syncHandles() {
343
- for (const handle of this.handles) {
344
- const [prev] = this.#neighborsOf(handle);
345
- setAttribute(handle, 'aria-orientation', this.#isHorizontal ? 'vertical' : 'horizontal');
395
+ #syncHandles(members) {
396
+ const {
397
+ panels,
398
+ handles
399
+ } = members;
400
+ const ariaOrientation = this.#isHorizontal ? 'vertical' : 'horizontal';
401
+ let index = 0;
402
+ for (const handle of handles) {
403
+ let following = panels[index];
404
+ while (following && precedes(following, handle)) {
405
+ index++;
406
+ following = panels[index];
407
+ }
408
+ const prev = panels[index - 1];
409
+ setAttribute(handle, 'aria-orientation', ariaOrientation);
346
410
  if (!prev) continue;
347
411
 
348
412
  // Panels render their own (component-owned, incrementing) id
@@ -352,34 +416,17 @@ class GroupState {
352
416
  setAttribute(handle, 'aria-valuenow', `${Math.round(this.#sizes.get(prev) ?? 0)}`);
353
417
  }
354
418
  }
355
- #notify() {
356
- this.#options.onLayoutChange()?.(this.sizes);
357
- }
358
-
359
- /**
360
- * The `data-collapsed` attribute is the source of truth for collapse
361
- * state (it is also the styling hook consumers use).
362
- */
363
- #isCollapsed(panel) {
364
- return isCollapsible(panel) && panel.hasAttribute('data-collapsed');
365
- }
366
- #setCollapsed(panel, collapsed) {
367
- if (collapsed === panel.hasAttribute('data-collapsed')) return;
368
- if (collapsed) {
369
- panel.setAttribute('data-collapsed', '');
370
- } else {
371
- panel.removeAttribute('data-collapsed');
372
- }
419
+ #notify(panels) {
420
+ this.#options.onLayoutChange()?.(panels.map(panel => this.#sizes.get(panel) ?? 0));
373
421
  }
374
422
 
375
423
  /**
376
424
  * Re-derive percentage sizes from actual rendered pixels.
377
425
  * Corrects any drift (e.g. from CSS min-sizes) before an interaction.
378
426
  */
379
- #syncSizesFromDOM() {
380
- const panels = this.panels;
427
+ #syncSizesFromDOM(panels, measured) {
381
428
  // collapsed panels are 0 even though their borders/padding measure larger
382
- const px = panels.map(panel => this.#isCollapsed(panel) ? 0 : this.#pixelSizeOf(panel));
429
+ const px = panels.map((panel, index) => isCollapsed(panel) ? 0 : measured?.[index] ?? this.#pixelSizeOf(panel));
383
430
  const total = px.reduce((sum, value) => sum + value, 0);
384
431
  if (total <= 0) return;
385
432
  panels.forEach((panel, index) => {
@@ -395,42 +442,26 @@ class GroupState {
395
442
  return this.#isHorizontal ? box.width : box.height;
396
443
  }
397
444
 
398
- /**
399
- * The panels immediately before and after the given handle element,
400
- * in document order.
401
- */
402
- #neighborsOf(handleElement) {
403
- let prev = null;
404
- let next = null;
405
- for (const panel of this.panels) {
406
- const position = handleElement.compareDocumentPosition(panel);
407
- if (position & Node.DOCUMENT_POSITION_PRECEDING) {
408
- prev = panel;
409
- } else if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
410
- next = panel;
411
- break;
412
- }
413
- }
414
- return [prev, next];
415
- }
416
-
417
445
  /**
418
446
  * Moves the boundary between the two panels by `requestedDelta` (%),
419
447
  * respecting both panels' min/max constraints.
420
448
  */
421
- #applyDelta(prev, next, basePrevSize, baseNextSize, requestedDelta) {
449
+ #applyDelta(prev, next, basePrevSize, baseNextSize, requestedDelta, members) {
422
450
  const total = basePrevSize + baseNextSize;
451
+ const prevMin = minSizeOf(prev);
452
+ const prevMax = maxSizeOf(prev);
453
+ const prevCollapsible = isCollapsible(prev);
423
454
  let target = basePrevSize + requestedDelta;
424
- if (isCollapsible(prev) && target < minSizeOf(prev)) {
455
+ if (prevCollapsible && target < prevMin) {
425
456
  /**
426
457
  * Collapsible panels snap: below half the minimum they close
427
458
  * entirely; between half and the minimum they hold at the minimum.
428
459
  * (This also keeps a collapsed panel closed when its handle is
429
460
  * dragged further in the closing direction.)
430
461
  */
431
- target = target < minSizeOf(prev) / 2 ? 0 : minSizeOf(prev);
462
+ target = target < prevMin / 2 ? 0 : prevMin;
432
463
  } else {
433
- target = clamp(target, minSizeOf(prev), maxSizeOf(prev));
464
+ target = clamp(target, prevMin, prevMax);
434
465
  }
435
466
 
436
467
  /**
@@ -443,7 +474,7 @@ class GroupState {
443
474
  target = clamp(target, nextMin, nextMax);
444
475
 
445
476
  // both panels' constraints cannot be satisfied at once
446
- if (!isCollapsible(prev) && (target < minSizeOf(prev) || target > maxSizeOf(prev))) return;
477
+ if (!prevCollapsible && (target < prevMin || target > prevMax)) return;
447
478
 
448
479
  /**
449
480
  * Nothing to do when the clamped result matches the current sizes
@@ -452,23 +483,25 @@ class GroupState {
452
483
  if (isSameSize(this.#sizes.get(prev), target) && isSameSize(this.#sizes.get(next), total - target)) {
453
484
  return;
454
485
  }
455
- if (isCollapsible(prev)) {
456
- if (target === 0 && !this.#isCollapsed(prev)) {
486
+ if (prevCollapsible) {
487
+ if (target === 0 && !isCollapsed(prev)) {
457
488
  this.#previousSizes.set(prev, basePrevSize);
458
489
  }
459
- this.#setCollapsed(prev, target === 0);
490
+ setCollapsed(prev, target === 0);
460
491
  }
461
492
  this.#sizes.set(prev, target);
462
493
  this.#sizes.set(next, total - target);
463
- this.#apply([prev, next]);
464
- this.#notify();
494
+ this.#apply([prev, next], members);
495
+ this.#notify(members.panels);
465
496
  }
466
497
  startDrag(handleElement, event) {
467
498
  if (event.button !== 0) return;
468
499
  if (this.#drag) return;
469
- const [prev, next] = this.#neighborsOf(handleElement);
500
+ const members = this.#members();
501
+ const [prev, next] = neighborsOf(handleElement, members.panels);
470
502
  if (!prev || !next) return;
471
- this.#syncSizesFromDOM();
503
+ const measured = members.panels.map(panel => this.#pixelSizeOf(panel));
504
+ this.#syncSizesFromDOM(members.panels, measured);
472
505
  const move = moveEvent => this.#dragMove(moveEvent);
473
506
  const end = endEvent => this.#endDrag(handleElement, endEvent);
474
507
  this.#drag = {
@@ -477,7 +510,8 @@ class GroupState {
477
510
  startPrevSize: this.#sizes.get(prev) ?? 0,
478
511
  startNextSize: this.#sizes.get(next) ?? 0,
479
512
  startCoordinate: this.#isHorizontal ? event.clientX : event.clientY,
480
- totalPx: this.panels.reduce((sum, panel) => sum + this.#pixelSizeOf(panel), 0),
513
+ totalPx: measured.reduce((sum, value) => sum + value, 0),
514
+ members,
481
515
  move,
482
516
  end
483
517
  };
@@ -490,9 +524,9 @@ class GroupState {
490
524
  } catch {
491
525
  // synthetic events (tests) may not have an active pointer
492
526
  }
493
- handleElement.addEventListener('pointermove', this.#drag.move);
494
- handleElement.addEventListener('pointerup', this.#drag.end);
495
- handleElement.addEventListener('pointercancel', this.#drag.end);
527
+ handleElement.addEventListener('pointermove', move);
528
+ handleElement.addEventListener('pointerup', end);
529
+ handleElement.addEventListener('pointercancel', end);
496
530
  handleElement.setAttribute('data-resizing', '');
497
531
  document.body.style.cursor = this.#isHorizontal ? 'col-resize' : 'row-resize';
498
532
  document.body.style.userSelect = 'none';
@@ -503,7 +537,7 @@ class GroupState {
503
537
  if (drag.totalPx <= 0) return;
504
538
  const coordinate = this.#isHorizontal ? event.clientX : event.clientY;
505
539
  const deltaPercent = (coordinate - drag.startCoordinate) / drag.totalPx * 100;
506
- this.#applyDelta(drag.prev, drag.next, drag.startPrevSize, drag.startNextSize, deltaPercent);
540
+ this.#applyDelta(drag.prev, drag.next, drag.startPrevSize, drag.startNextSize, deltaPercent, drag.members);
507
541
  }
508
542
  #endDrag(handleElement, event) {
509
543
  const drag = this.#drag;
@@ -526,41 +560,41 @@ class GroupState {
526
560
  * Keyboard support for the WAI-ARIA window-splitter pattern.
527
561
  */
528
562
  handleKeyDown(handleElement, event) {
529
- const [prev, next] = this.#neighborsOf(handleElement);
563
+ const members = this.#members();
564
+ const [prev, next] = neighborsOf(handleElement, members.panels);
530
565
  if (!prev || !next) return;
531
566
  if (event.key === 'Enter') {
532
- this.#toggleCollapse(prev, next);
567
+ this.#toggleCollapse(prev, next, members);
533
568
  return;
534
569
  }
535
570
  const step = event.shiftKey ? KEYBOARD_STEP_COARSE : KEYBOARD_STEP;
536
571
  const isHorizontal = this.#isHorizontal;
537
- let delta = null;
572
+ let toDelta = null;
538
573
  switch (event.key) {
539
574
  case 'ArrowLeft':
540
- if (isHorizontal) delta = -step;
575
+ if (isHorizontal) toDelta = () => -step;
541
576
  break;
542
577
  case 'ArrowRight':
543
- if (isHorizontal) delta = step;
578
+ if (isHorizontal) toDelta = () => step;
544
579
  break;
545
580
  case 'ArrowUp':
546
- if (!isHorizontal) delta = -step;
581
+ if (!isHorizontal) toDelta = () => -step;
547
582
  break;
548
583
  case 'ArrowDown':
549
- if (!isHorizontal) delta = step;
584
+ if (!isHorizontal) toDelta = () => step;
550
585
  break;
551
586
  case 'Home':
552
- this.#syncSizesFromDOM();
553
- delta = minSizeOf(prev) - (this.#sizes.get(prev) ?? 0);
587
+ toDelta = prevSize => minSizeOf(prev) - prevSize;
554
588
  break;
555
589
  case 'End':
556
- this.#syncSizesFromDOM();
557
- delta = maxSizeOf(prev) - (this.#sizes.get(prev) ?? 0);
590
+ toDelta = prevSize => maxSizeOf(prev) - prevSize;
558
591
  break;
559
592
  }
560
- if (delta === null) return;
593
+ if (!toDelta) return;
561
594
  event.preventDefault();
562
- this.#syncSizesFromDOM();
563
- this.#applyDelta(prev, next, this.#sizes.get(prev) ?? 0, this.#sizes.get(next) ?? 0, delta);
595
+ this.#syncSizesFromDOM(members.panels);
596
+ const prevSize = this.#sizes.get(prev) ?? 0;
597
+ this.#applyDelta(prev, next, prevSize, this.#sizes.get(next) ?? 0, toDelta(prevSize), members);
564
598
  }
565
599
 
566
600
  /**
@@ -569,27 +603,27 @@ class GroupState {
569
603
  *
570
604
  * Only does anything when the preceding panel is `@collapsible`.
571
605
  */
572
- #toggleCollapse(prev, next) {
606
+ #toggleCollapse(prev, next, members) {
573
607
  if (!isCollapsible(prev)) return;
574
- this.#syncSizesFromDOM();
608
+ this.#syncSizesFromDOM(members.panels);
575
609
  const prevSize = this.#sizes.get(prev) ?? 0;
576
610
  const nextSize = this.#sizes.get(next) ?? 0;
577
- if (this.#isCollapsed(prev)) {
611
+ if (isCollapsed(prev)) {
578
612
  const preferred = this.#previousSizes.get(prev) ?? requestedSizeOf(prev) ?? Math.max(minSizeOf(prev), 10);
579
613
  const available = nextSize - minSizeOf(next);
580
614
  const restored = Math.min(preferred, available);
581
615
  if (restored <= 0) return;
582
- this.#setCollapsed(prev, false);
616
+ setCollapsed(prev, false);
583
617
  this.#sizes.set(prev, restored);
584
618
  this.#sizes.set(next, nextSize - restored);
585
619
  } else {
586
620
  this.#previousSizes.set(prev, prevSize);
587
- this.#setCollapsed(prev, true);
621
+ setCollapsed(prev, true);
588
622
  this.#sizes.set(prev, 0);
589
623
  this.#sizes.set(next, nextSize + prevSize);
590
624
  }
591
- this.#apply([prev, next]);
592
- this.#notify();
625
+ this.#apply([prev, next], members);
626
+ this.#notify(members.panels);
593
627
  }
594
628
  }
595
629