@fusion.dev/helix-headless 0.182.2 → 0.184.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,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, anchorAt, commaList, customElement, elementById, HelixElement, HelixLoadMoreEvent, HelixRangeChangeEvent, inRowOrder, isRtl, pressedOwnControl, rangeAdd, requestValue, SCROLLER_RULE, searchableById, selectsAll, Typeahead, toggleValue, typesCharacter, VIRTUAL_RULE, VirtualizeController, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, anchorAt, commaList, customElement, elementById, HelixElement, HelixLoadMoreEvent, HelixRangeChangeEvent, inRowOrder, isRtl, pressedOwnControl, rangeAdd, requestValue, SCROLLER_RULE, searchableById, selectsAll, Typeahead, toggleValue, typesCharacter, VIRTUAL_RULE, VirtualizeController, Waterfall, words, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing, render } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import { holdControls, releaseControls } from '../controls.js';
@@ -13,6 +13,7 @@ const nextSectionId = () => {
13
13
  return `virtual-list-${sectionCount}`;
14
14
  };
15
15
  const ROWS = ':scope > [data-index], :scope > [data-section] > [data-index]';
16
+ const LINE = 'helix-line-';
16
17
  let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixElement {
17
18
  constructor() {
18
19
  super(...arguments);
@@ -24,6 +25,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
24
25
  this.gap = 0;
25
26
  this.keyField = '';
26
27
  this.label = '';
28
+ this.layout = 'grid';
27
29
  this.loading = false;
28
30
  this.loadingLabel = 'Loading more';
29
31
  this.measured = false;
@@ -54,7 +56,12 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
54
56
  horizontal: () => this.horizontal,
55
57
  measured: () => this.measured,
56
58
  rows: () => this.lines(),
59
+ waterfall: () => {
60
+ this.lanes.lanes = this.columnCount;
61
+ return this.waterfall ? this.lanes : undefined;
62
+ },
57
63
  });
64
+ this.lanes = new Waterfall();
58
65
  this.boxWidth = 0;
59
66
  this.measuredAcross = 1;
60
67
  this.handleKeydown = (event) => {
@@ -97,7 +104,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
97
104
  }
98
105
  }
99
106
  const extend = event.shiftKey && this.selectionMode === 'multiple';
100
- if (event.altKey || (toggle && !replacing) || (event.shiftKey && !extend)) {
107
+ const ends = event.key === 'Home' || event.key === 'End';
108
+ const lined = this.lined;
109
+ if (event.altKey || (toggle && !replacing && !(lined && ends)) || (event.shiftKey && !extend)) {
101
110
  return;
102
111
  }
103
112
  const count = this.data.length;
@@ -112,13 +121,19 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
112
121
  : event.key === 'ArrowDown' || event.key === 'ArrowUp'
113
122
  ? ''
114
123
  : event.key;
115
- const stepped = stepIndex(key, at, count, last - first - 1, {
116
- across: this.across,
117
- forward,
118
- });
124
+ const disabled = (one) => this.isDisabled(one);
125
+ const across = this.across;
126
+ const start = at - (at % across);
127
+ const stepped = lined && ends && !toggle
128
+ ? key === 'Home'
129
+ ? start
130
+ : Math.min(start + across, count) - 1
131
+ : this.waterfall && !ends
132
+ ? this.inWaterfall(key, at, count, forward)
133
+ : stepIndex(key, at, count, last - first - 1, { across, forward });
119
134
  const next = stepped === undefined
120
135
  ? undefined
121
- : skipDisabled(key, at, stepped, count, (one) => this.isDisabled(one), this.across);
136
+ : skipDisabled(key, at, stepped, count, disabled, this.waterfall ? 1 : across);
122
137
  if (next === undefined) {
123
138
  if (stepped !== undefined) {
124
139
  event.preventDefault();
@@ -172,6 +187,8 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
172
187
  this.requestUpdate();
173
188
  };
174
189
  this.visited = false;
190
+ this.placedWaterfall = false;
191
+ this.drawnLines = [];
175
192
  this.sectionId = nextSectionId();
176
193
  }
177
194
  static { this.markRules = [SCROLLER_RULE, VIRTUAL_RULE]; }
@@ -182,7 +199,20 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
182
199
  get space() {
183
200
  return Number.isFinite(this.gap) && this.gap > 0 ? this.gap : 0;
184
201
  }
202
+ get waterfall() {
203
+ return this.layout === 'waterfall';
204
+ }
205
+ get lined() {
206
+ return (!this.selectionMode &&
207
+ !this.waterfall &&
208
+ !this.horizontal &&
209
+ this.sections === undefined &&
210
+ (this.across > 1 || this.minColumnWidth > 0));
211
+ }
185
212
  get across() {
213
+ return this.waterfall ? 1 : this.columnCount;
214
+ }
215
+ get columnCount() {
186
216
  if (this.minColumnWidth > 0 && this.boxWidth > 0) {
187
217
  const space = this.space;
188
218
  return Math.max(1, Math.floor((this.boxWidth + space) / (this.minColumnWidth + space)));
@@ -191,7 +221,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
191
221
  }
192
222
  watchWidth() {
193
223
  const viewport = this.partNamed('viewport');
194
- if (this.minColumnWidth <= 0 || !viewport || typeof ResizeObserver === 'undefined') {
224
+ if ((this.minColumnWidth <= 0 && !this.waterfall) ||
225
+ !viewport ||
226
+ typeof ResizeObserver === 'undefined') {
195
227
  this.widthWatch?.disconnect();
196
228
  this.widthWatch = undefined;
197
229
  return;
@@ -218,11 +250,11 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
218
250
  return sections ? entryOf(sections, at) : Math.floor(at / this.across);
219
251
  }
220
252
  get horizontal() {
221
- return this.orientation === 'horizontal' && this.across === 1;
253
+ return this.orientation === 'horizontal' && this.across === 1 && !this.waterfall;
222
254
  }
223
255
  get sections() {
224
256
  const data = this.data;
225
- if (this.sectionField === '' || data.length === 0 || this.across !== 1) {
257
+ if (this.sectionField === '' || data.length === 0 || this.columnCount !== 1 || this.waterfall) {
226
258
  return undefined;
227
259
  }
228
260
  const read = this.sectionsRead;
@@ -275,7 +307,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
275
307
  this.readFor = this.for;
276
308
  this.read();
277
309
  }
278
- const across = this.across;
310
+ const across = this.waterfall ? `${this.columnCount} ${this.boxWidth}` : this.across;
279
311
  if (across !== this.measuredAcross) {
280
312
  this.measuredAcross = across;
281
313
  this.virtualize.forget();
@@ -450,6 +482,16 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
450
482
  rowFor(index) {
451
483
  return ([...this.querySelectorAll(ROWS)].find((row) => row.dataset.index === String(index)) ?? null);
452
484
  }
485
+ inWaterfall(key, at, count, forward) {
486
+ const way = key === forward ? 'next' : key === 'ArrowLeft' || key === 'ArrowRight' ? 'previous' : key;
487
+ const box = this.partNamed('viewport');
488
+ let to = this.lanes.step(way, at, count, box?.clientHeight ?? 0);
489
+ const again = way === 'PageDown' ? 'ArrowDown' : way === 'PageUp' ? 'ArrowUp' : way;
490
+ while (to !== undefined && this.isDisabled(to)) {
491
+ to = this.lanes.step(again, to, count, 0);
492
+ }
493
+ return to;
494
+ }
453
495
  keyAt(at) {
454
496
  return keyOf(this.data[at], at, this.keyField);
455
497
  }
@@ -530,7 +572,22 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
530
572
  : undefined;
531
573
  }
532
574
  mark(element, at, chosen, off) {
533
- element.setAttribute('role', this.selectionMode ? 'option' : 'listitem');
575
+ const lined = this.lined;
576
+ element.setAttribute('role', this.selectionMode ? 'option' : lined ? 'gridcell' : 'listitem');
577
+ const across = this.across;
578
+ for (const [name, value] of [
579
+ ['aria-colindex', lined ? (at % across) + 1 : undefined],
580
+ ['aria-posinset', lined ? undefined : at + 1],
581
+ ['aria-setsize', lined ? undefined : this.data.length],
582
+ ['slot', lined ? `${LINE}${Math.floor(at / across)}` : undefined],
583
+ ]) {
584
+ if (value === undefined) {
585
+ element.removeAttribute(name);
586
+ }
587
+ else {
588
+ element.setAttribute(name, String(value));
589
+ }
590
+ }
534
591
  const selected = Boolean(this.selectionMode) && chosen.has(this.keyAt(at));
535
592
  if (this.selectionMode) {
536
593
  element.setAttribute('aria-selected', String(selected));
@@ -549,8 +606,6 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
549
606
  element.toggleAttribute('data-disabled', disabled);
550
607
  }
551
608
  stamp(element, at) {
552
- element.setAttribute('aria-posinset', String(at + 1));
553
- element.setAttribute('aria-setsize', String(this.data.length));
554
609
  element.setAttribute('data-index', String(at));
555
610
  }
556
611
  fill(elements, at) {
@@ -595,12 +650,13 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
595
650
  this.partNamed('after')?.style.setProperty(size, `${spacers.after}px`);
596
651
  }
597
652
  const sections = this.sections;
598
- const regroup = sections !== this.placedSections;
653
+ const regroup = sections !== this.placedSections || this.waterfall !== this.placedWaterfall;
599
654
  this.placedSections = sections;
655
+ this.placedWaterfall = this.waterfall;
600
656
  const children = [
601
657
  ...[...this.children].filter((child) => child !== template &&
602
658
  child instanceof HTMLElement &&
603
- !child.hasAttribute('slot') &&
659
+ (child.dataset.index !== undefined || !child.hasAttribute('slot')) &&
604
660
  !child.hasAttribute('data-section')),
605
661
  ...this.querySelectorAll(':scope > [data-section] > [data-index]'),
606
662
  ];
@@ -660,9 +716,23 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
660
716
  });
661
717
  const chosen = this.chosen;
662
718
  const off = this.disabled;
719
+ const lines = this.lined ? [...new Set(plan.wanted.map((at) => this.lineOf(at)))] : [];
720
+ if (lines.join() !== this.drawnLines.join()) {
721
+ this.drawnLines = lines;
722
+ this.requestUpdate();
723
+ }
724
+ const gap = this.space;
725
+ const columns = this.columnCount;
663
726
  for (const [at, group] of drawn) {
664
727
  for (const element of group) {
665
728
  this.mark(element, at, chosen, off);
729
+ if (this.waterfall) {
730
+ const style = element.style;
731
+ style.setProperty('position', 'absolute');
732
+ style.setProperty('inset-block-start', `${this.virtualize.offsetOf(at)}px`);
733
+ style.setProperty('inset-inline-start', `calc((100% + ${gap}px) * ${this.lanes.lane(at)} / ${columns})`);
734
+ style.setProperty('inline-size', `calc((100% - ${gap * (columns - 1)}px) / ${columns})`);
735
+ }
666
736
  }
667
737
  const stop = at === this.active;
668
738
  group[0]?.setAttribute('tabindex', stop ? '0' : '-1');
@@ -745,6 +815,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
745
815
  return groups;
746
816
  }
747
817
  spacerSizes() {
818
+ if (this.waterfall) {
819
+ return { after: 0, before: 0 };
820
+ }
748
821
  const kept = this.hasUpdated ? this.keptIndex() : undefined;
749
822
  const room = this.virtualize.spacers(kept === undefined ? undefined : this.lineOf(kept));
750
823
  const sections = this.sections;
@@ -779,14 +852,20 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
779
852
  const size = horizontal ? 'inline-size' : 'block-size';
780
853
  const flex = horizontal ? '; flex: none' : '';
781
854
  const gap = this.space;
855
+ const grid = gap === 0 ? '' : `; gap: ${gap}px; padding-block-end: ${gap}px`;
782
856
  const empty = this.readFor !== undefined &&
783
857
  !this.loading &&
784
858
  this.data.length === 0 &&
785
859
  (this.emptyLabel !== '' || this.querySelector(':scope > [slot="empty"]') !== null);
860
+ const lined = this.lined;
861
+ const own = (content) => lined ? html `<div role="gridcell">${content}</div>` : content;
862
+ const role = this.selectionMode ? 'option' : lined ? 'row' : 'listitem';
786
863
  return html `<div
787
864
  data-scroller
788
865
  part="viewport"
789
- role=${this.selectionMode ? 'listbox' : 'list'}
866
+ role=${this.selectionMode ? 'listbox' : lined ? 'grid' : 'list'}
867
+ aria-colcount=${lined ? across : nothing}
868
+ aria-rowcount=${lined ? Math.ceil(this.data.length / across) : nothing}
790
869
  aria-multiselectable=${this.selectionMode === 'multiple' ? 'true' : nothing}
791
870
  aria-busy=${this.loading ? 'true' : nothing}
792
871
  aria-label=${this.label}
@@ -798,24 +877,37 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
798
877
  }}
799
878
  >
800
879
  <div part="before" style="${size}: ${before}px${flex}"></div>
880
+ ${lined
881
+ ? html `<div style=${`display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${grid}`}>
882
+ ${this.drawnLines.map((line) => html `<div
883
+ aria-rowindex=${line + 1}
884
+ role="row"
885
+ style="display: grid; grid-column: 1 / -1; grid-template-columns: subgrid"
886
+ >
887
+ <slot name=${`${LINE}${line}`}></slot>
888
+ </div>`)}
889
+ </div>`
890
+ : nothing}
801
891
  <slot
802
- style=${horizontal
803
- ? `display: flex; flex: none${gap === 0 ? '' : `; column-gap: ${gap}px; padding-inline-end: ${gap}px`}`
804
- : across === 1
805
- ? gap === 0
806
- ? nothing
807
- : `display: flex; flex-direction: column; row-gap: ${gap}px; padding-block-end: ${gap}px`
808
- : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${gap === 0 ? '' : `; gap: ${gap}px; padding-block-end: ${gap}px`}`}
892
+ style=${this.waterfall
893
+ ? `display: block; position: relative; block-size: ${this.virtualize.offsetOf(this.data.length)}px`
894
+ : horizontal
895
+ ? `display: flex; flex: none${gap === 0 ? '' : `; column-gap: ${gap}px; padding-inline-end: ${gap}px`}`
896
+ : across === 1 || lined
897
+ ? gap === 0
898
+ ? nothing
899
+ : `display: flex; flex-direction: column; row-gap: ${gap}px; padding-block-end: ${gap}px`
900
+ : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${grid}`}
809
901
  ></slot>
810
902
  <div part="after" style="${size}: ${after}px${flex}"></div>
811
903
  ${this.loading
812
- ? html `<div part="loading" role=${this.selectionMode ? 'option' : 'listitem'}>
813
- <slot name="loading">${this.loadingLabel}</slot>
904
+ ? html `<div part="loading" role=${role}>
905
+ ${own(html `<slot name="loading">${this.loadingLabel}</slot>`)}
814
906
  </div>`
815
907
  : nothing}
816
908
  ${empty
817
- ? html `<div part="empty" role=${this.selectionMode ? 'option' : 'listitem'}>
818
- <slot name="empty">${this.emptyLabel}</slot>
909
+ ? html `<div part="empty" role=${role}>
910
+ ${own(html `<slot name="empty">${this.emptyLabel}</slot>`)}
819
911
  </div>`
820
912
  : nothing}
821
913
  </div>`;
@@ -848,6 +940,9 @@ __decorate([
848
940
  __decorate([
849
941
  property()
850
942
  ], HelixHeadlessVirtualList.prototype, "label", void 0);
943
+ __decorate([
944
+ property()
945
+ ], HelixHeadlessVirtualList.prototype, "layout", void 0);
851
946
  __decorate([
852
947
  property({ type: Boolean })
853
948
  ], HelixHeadlessVirtualList.prototype, "loading", void 0);