@fusion.dev/helix-headless 0.182.2 → 0.183.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.
@@ -61,6 +61,32 @@ export interface VirtualListRange {
61
61
  * smallest item size, and follows the box as it resizes. `gap` spaces the rows, and the columns
62
62
  * of a grid, by a number of pixels the list counts in each row's place.
63
63
  *
64
+ * A grid without `selection-mode` is a grid to a reader, as the APG's grid
65
+ * and React Aria's grid list are: the box is a `grid` with `aria-rowcount`
66
+ * and `aria-colcount`, each line drawn is a `row` with `aria-rowindex`, and
67
+ * each item a `gridcell` with `aria-colindex`. Each line is a slot of the
68
+ * list's own, so your rows stay its children: give an item one element. With
69
+ * `selection-mode` it stays a list box of options, as React Aria's list box
70
+ * in a grid layout does.
71
+ *
72
+ * **A waterfall.** `layout="waterfall"` puts each item, in the order of the
73
+ * set, under the column that ends highest so far, the leftmost of a tie, as a
74
+ * wall of photos of different heights is, and as React Aria's waterfall
75
+ * layout and TanStack Virtual's lanes do. `columns`, or `min-column-width`,
76
+ * says how many columns. Every item is measured, at the width of its column,
77
+ * and placed at its column and its height, so items of any height leave no
78
+ * holes. The list draws the items that reach into the box, and the item
79
+ * holding focus. A reader hears a list, or a list box, in the order of the
80
+ * set, which is also the order Tab and a screen reader's own keys take.
81
+ *
82
+ * In a waterfall, Down and Up move along the column to the next or the
83
+ * previous item in it, Right and Left move to the next or the previous
84
+ * column, to the item there that sits beside this one the most, or the
85
+ * nearest above where none does, and Page Down and Page Up move a box down
86
+ * or up the column. Home and End go to the first and the last item of the
87
+ * set, and Shift with a key takes in the items between in the order of the
88
+ * set.
89
+ *
64
90
  * **Across.** `orientation="horizontal"` lays the items out along a line and
65
91
  * scrolls it sideways, as a strip of cards does, windowed the same way:
66
92
  * `estimate` is then an item's width, and the box needs an inline size.
@@ -79,10 +105,11 @@ export interface VirtualListRange {
79
105
  *
80
106
  * | Key | What it does |
81
107
  * |---|---|
82
- * | Down, Up | Moves to the next or the previous item, or in a grid to the item in the same column on the next or the previous line. |
83
- * | Right, Left | In a grid, moves to the next or the previous item along the line, the other way round on a right-to-left page. In a list that runs across, moves to the next or the previous item the same way, and Down and Up are left to the row. |
84
- * | Home, End | Moves to the first or the last item. |
85
- * | Page Down, Page Up | Moves by the number of items, or of lines in a grid, on screen. |
108
+ * | Down, Up | Moves to the next or the previous item, or in a grid to the item in the same column on the next or the previous line, or in a waterfall to the next or the previous item down the same column. |
109
+ * | Right, Left | In a grid, moves to the next or the previous item along the line, the other way round on a right-to-left page. In a waterfall, moves to the item beside in the next or the previous column. In a list that runs across, moves to the next or the previous item the same way, and Down and Up are left to the row. |
110
+ * | Home, End | Moves to the first or the last item. In a grid without `selection-mode`, to the first or the last item of the line. |
111
+ * | Control or Command with Home or End | In a grid without `selection-mode`, moves to the first or the last item. |
112
+ * | Page Down, Page Up | Moves by the number of items, or of lines in a grid, on screen, or in a waterfall a box down or up the column. |
86
113
  * | A letter or a word | Moves to the next item whose text starts with what you typed. |
87
114
  * | Escape | Lets go of every chosen item, in a list with `selection-mode`. |
88
115
  * | Alt with Up or Down | In a `reorderable` list, moves the item a place. Alt with Home or End moves it to either end. |
@@ -97,8 +124,9 @@ export interface VirtualListRange {
97
124
  * or the item itself when it is a string or a number.
98
125
  *
99
126
  * Each row says its place in the whole set with `aria-posinset` and
100
- * `aria-setsize`, so a screen reader says "4,000 of 10,000". The row holding
101
- * focus stays drawn when it scrolls out.
127
+ * `aria-setsize`, so a screen reader says "4,000 of 10,000", and each cell of
128
+ * a grid its line and its column. The row holding focus stays drawn when it
129
+ * scrolls out.
102
130
  *
103
131
  * **Selecting.** `selection-mode` makes the list a list box a reader chooses
104
132
  * from: `single` selects one item and `multiple` any number. Space or a press
@@ -267,6 +295,15 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
267
295
  * wins over this.
268
296
  */
269
297
  label: string;
298
+ /**
299
+ * How items of more than one column are laid out. `grid`, the default, puts
300
+ * them in lines of equal columns. `waterfall` puts each item, in the order
301
+ * of the set, under the column that ends highest, as a wall of photos of
302
+ * different heights is: `columns`, or `min-column-width`, says how many
303
+ * columns. Rows of a waterfall are measured, and Up and Down move along a
304
+ * column.
305
+ */
306
+ layout: 'grid' | 'waterfall';
270
307
  /**
271
308
  * Marks the list as busy fetching more items, and draws a row after the last
272
309
  * item that says so.
@@ -300,7 +337,7 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
300
337
  * Which way the list runs. `horizontal` lays the items out along a line
301
338
  * and scrolls it sideways: Left and Right move between items, the other
302
339
  * way round on a right-to-left page, and `estimate` is an item's width. A
303
- * grid always runs down.
340
+ * grid and a waterfall always run down.
304
341
  */
305
342
  orientation: 'horizontal' | 'vertical';
306
343
  /**
@@ -324,7 +361,8 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
324
361
  * with an arrow moves the row the keys are on, and a pointer drags a row.
325
362
  * The list moves no item itself: you move your `items` on `helix-reorder`.
326
363
  * Without that import it does nothing, so a list that never moves items
327
- * carries none of the code. A list of one column, without sections.
364
+ * carries none of the code. A list of one column, without sections or a
365
+ * waterfall.
328
366
  */
329
367
  reorderable: boolean;
330
368
  /**
@@ -422,24 +460,42 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
422
460
  get range(): VirtualListRange;
423
461
  /** The gap in pixels, none where it is not a positive number. */
424
462
  private get space();
463
+ /** Where each item of a waterfall stands, which the controller places. */
464
+ private readonly lanes;
465
+ /** Whether the items are laid out as a waterfall, each down a column. */
466
+ private get waterfall();
425
467
  /**
426
- * How many items a line holds: as many as fit at `min-column-width` where
427
- * it is set and the box has a width, and `columns` otherwise, at least one.
468
+ * Whether the list is a grid a reader reads, and not a list box: lines of
469
+ * columns with no `selection-mode`, which are `grid`, `row` and `gridcell`
470
+ * to a reader, as the APG's grid and React Aria's grid list are.
471
+ */
472
+ private get lined();
473
+ /**
474
+ * How many items a line holds, which a waterfall draws one at a time: its
475
+ * columns are not lines.
428
476
  */
429
477
  private get across();
478
+ /**
479
+ * How many columns there are: as many as fit at `min-column-width` where
480
+ * it is set and the box has a width, and `columns` otherwise, at least one.
481
+ */
482
+ private get columnCount();
430
483
  /** How wide the box is, read for `min-column-width`. */
431
484
  private boxWidth;
432
485
  /** Watches the box's width, for `min-column-width`. */
433
486
  private widthWatch;
434
- /** The items to a line the measurements were taken at. */
487
+ /** The items to a line the measurements were taken at, or a waterfall's columns and width. */
435
488
  private measuredAcross;
436
- /** Watches the box for its width while `min-column-width` asks for it. */
489
+ /**
490
+ * Watches the box for its width while `min-column-width` asks for it, or a
491
+ * waterfall, whose items are as tall as their width makes them.
492
+ */
437
493
  private watchWidth;
438
494
  /** The items a span of lines holds. */
439
495
  private itemsOn;
440
496
  /** The line item `at` is on: its entry in a list with sections. */
441
497
  private lineOf;
442
- /** Whether the list runs across, which a grid never does. */
498
+ /** Whether the list runs across, which a grid and a waterfall never do. */
443
499
  private get horizontal();
444
500
  /** The sections and the items they were read from, kept until either changes. */
445
501
  private sectionsRead;
@@ -541,6 +597,14 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
541
597
  * is that control's, so a field in a row keeps its arrows.
542
598
  */
543
599
  private readonly handleKeydown;
600
+ /**
601
+ * Where a key goes in a waterfall, through the columns `Waterfall.step`
602
+ * reads: along the column for Down, Up, Page Down and Page Up, and to the
603
+ * item beside in the next column or the one before for the keys along the
604
+ * line, the other way round on a right-to-left page. Past a disabled item,
605
+ * on the same way.
606
+ */
607
+ private inWaterfall;
544
608
  /** The key of item `at` in `value`. See `keyField`. */
545
609
  private keyAt;
546
610
  /** @internal Every item's key, in the order of the set, which the reorder entry moves. */
@@ -600,15 +664,25 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
600
664
  private keptIndex;
601
665
  /**
602
666
  * Writes a row's role, and whether it is chosen in a list that selects:
603
- * `option` with `aria-selected` there, and `listitem` otherwise, and
604
- * `aria-disabled` on an item `disabled-keys` names. Every pass,
605
- * since a choice changes no row's content.
667
+ * `option` with `aria-selected` there, `gridcell` in a grid, and `listitem`
668
+ * otherwise, and `aria-disabled` on an item `disabled-keys` names. Every
669
+ * pass, since a choice changes no row's content.
670
+ *
671
+ * Its place too: `aria-posinset` and `aria-setsize` in a list, and in a
672
+ * grid its column, with the line's `row` given by the slot it is drawn in.
606
673
  *
607
674
  * `data-selected` and `data-disabled` say the same to a stylesheet, as
608
675
  * React Aria's rows do, so one rule reads the same in either kind of list.
609
676
  */
610
677
  private mark;
611
- /** Writes a row's place in the set. */
678
+ /**
679
+ * Writes which item a row drew, for the controller to measure it against:
680
+ * the real index, the same one `aria-posinset` carries one higher.
681
+ *
682
+ * `mark` writes the place a reader hears from it, the real index and the
683
+ * real total, not the position in the window. A reader arrowing through
684
+ * this should hear where they are in the set.
685
+ */
612
686
  private stamp;
613
687
  /** Fills a drawn row from its item, by `renderItem` or by the template's holes. */
614
688
  private fill;
@@ -642,6 +716,10 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
642
716
  private place;
643
717
  /** The sections the rows were last placed in. */
644
718
  private placedSections;
719
+ /** Whether the rows were last placed in a waterfall. */
720
+ private placedWaterfall;
721
+ /** The lines of a grid drawn on the last pass, each a `row` the render draws a slot in. */
722
+ private drawnLines;
645
723
  /** The function the drawn headers were drawn by, so a new one draws them again. */
646
724
  private headedBy;
647
725
  /** Ties each header's id to this list. */
@@ -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);