@fusion.dev/helix-headless 0.165.0 → 0.167.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.
Files changed (57) hide show
  1. package/custom-elements.json +3235 -534
  2. package/dist/callout/root/callout-root.d.ts +8 -2
  3. package/dist/callout/root/callout-root.js +14 -1
  4. package/dist/canvas/arrange.d.ts +32 -0
  5. package/dist/canvas/arrange.js +144 -0
  6. package/dist/canvas/paste.d.ts +25 -0
  7. package/dist/canvas/paste.js +58 -0
  8. package/dist/canvas/root/canvas-root.d.ts +60 -3
  9. package/dist/canvas/root/canvas-root.js +166 -2
  10. package/dist/chat/composer/chat-composer.js +5 -1
  11. package/dist/code-block/root/code-block-root.d.ts +63 -0
  12. package/dist/code-block/root/code-block-root.js +117 -6
  13. package/dist/combobox/root/combobox-root.d.ts +6 -1
  14. package/dist/combobox/root/combobox-root.js +2 -2
  15. package/dist/date-input/root/date-input-root.d.ts +1 -1
  16. package/dist/element-events.generated.js +17 -2
  17. package/dist/file-input/root/file-input-root.d.ts +1 -1
  18. package/dist/file-input/root/file-input-root.js +1 -1
  19. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  20. package/dist/filter-input/menu/filter-menu.js +1 -1
  21. package/dist/format-date/root/format-date-root.d.ts +10 -3
  22. package/dist/format-date/root/format-date-root.js +6 -1
  23. package/dist/listbox/root/listbox-root.d.ts +12 -1
  24. package/dist/listbox/root/listbox-root.js +15 -6
  25. package/dist/menu/item/menu-item.d.ts +20 -0
  26. package/dist/menu/item/menu-item.js +53 -20
  27. package/dist/menu/root/menu-root.d.ts +84 -1
  28. package/dist/menu/root/menu-root.js +187 -44
  29. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -0
  30. package/dist/object-inspector/root/object-inspector-root.js +5 -0
  31. package/dist/select/root/select-root.js +1 -1
  32. package/dist/steps/root/steps-root.d.ts +7 -0
  33. package/dist/steps/root/steps-root.js +7 -0
  34. package/dist/table/grid-controller.d.ts +10 -0
  35. package/dist/table/grid-controller.js +7 -0
  36. package/dist/table/root/table-root.d.ts +71 -1
  37. package/dist/table/root/table-root.js +122 -7
  38. package/dist/table/row/table-row.d.ts +17 -0
  39. package/dist/table/row/table-row.js +23 -0
  40. package/dist/time-input/root/time-input-root.d.ts +2 -1
  41. package/dist/time-input/root/time-input-root.js +1 -1
  42. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  43. package/dist/time-picker/root/time-picker-root.js +79 -21
  44. package/dist/video/root/video-root.d.ts +2 -0
  45. package/dist/video/root/video-root.js +4 -2
  46. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  47. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  48. package/dist/video-playlist/root/video-playlist-root.js +15 -1
  49. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  50. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  51. package/dist/virtual-list/sections.d.ts +27 -0
  52. package/dist/virtual-list/sections.js +43 -0
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +716 -96
  56. package/types/vue.d.ts +148 -12
  57. package/types/words.d.ts +45 -0
@@ -594,7 +594,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
594
594
  }
595
595
  return html `<${this.tag('menu')} part="captions-menu"
596
596
  exportparts="back: captions-menu-back, back-mark: captions-menu-back-mark, empty: captions-menu-empty,
597
- header: captions-menu-header, live: captions-menu-live, loading: captions-menu-loading, surface: captions-menu-surface,
597
+ header: captions-menu-header, live: captions-menu-live, loading: captions-menu-loading,
598
+ search-field: captions-menu-search-field, surface: captions-menu-surface,
598
599
  title: captions-menu-title" label=${this.subtitlesLabel} placement="top-end"
599
600
  @toggle=${(event) => this.heardMenu('captions', event)}>
600
601
  <button
@@ -804,7 +805,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
804
805
  ? nothing
805
806
  : html `<${this.tag('menu')} part="overflow"
806
807
  exportparts="back: overflow-back, back-mark: overflow-back-mark, empty: overflow-empty,
807
- header: overflow-header, live: overflow-live, loading: overflow-loading, surface: overflow-surface,
808
+ header: overflow-header, live: overflow-live, loading: overflow-loading,
809
+ search-field: overflow-search-field, surface: overflow-surface,
808
810
  title: overflow-title" label=${this.moreLabel} placement="top-end"
809
811
  @toggle=${(event) => this.heardMenu('overflow', event)}>
810
812
  <button
@@ -38,6 +38,7 @@ import '../../menu/index.js';
38
38
  * @csspart menu-header - The bar above a submenu, holding the back button and the submenu's name.
39
39
  * @csspart menu-live - The inner menu's `live` part, the region screen readers announce its empty and loading messages from.
40
40
  * @csspart menu-loading - Shown while the items are loading.
41
+ * @csspart menu-search-field - The inner menu's filter field, drawn with `filterable`.
41
42
  * @csspart menu-surface - The floating panel the menu is drawn on.
42
43
  * @csspart menu-title - The name of the submenu that is shown.
43
44
  * @csspart poster - An entry's poster image, if it has one.
@@ -84,7 +84,7 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
84
84
  renderMenu() {
85
85
  return html `<${this.tag('menu')} part="menu"
86
86
  exportparts="back: menu-back, back-mark: menu-back-mark, empty: menu-empty, header: menu-header,
87
- live: menu-live, loading: menu-loading, surface: menu-surface, title: menu-title" label=${this.label} placement=${this.placement}
87
+ live: menu-live, loading: menu-loading, search-field: menu-search-field, surface: menu-surface, title: menu-title" label=${this.label} placement=${this.placement}
88
88
  @toggle=${this.heardToggle}>
89
89
  <slot name="trigger" slot="trigger">
90
90
  <button
@@ -47,8 +47,16 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
47
47
  if (event.defaultPrevented) {
48
48
  return;
49
49
  }
50
+ const value = event.detail.value;
51
+ const player = this.player;
52
+ if (value === this.active) {
53
+ if (player && player.paused !== false) {
54
+ void Promise.resolve(player.play()).catch(() => { });
55
+ }
56
+ return;
57
+ }
50
58
  this.chased = true;
51
- this.requestActive(event.detail.value, 'press');
59
+ this.requestActive(value, 'press');
52
60
  };
53
61
  this.handleFocusIn = (event) => {
54
62
  const entry = event.target;
@@ -259,6 +267,12 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
259
267
  if (changed.has('shuffle') && !this.shuffle) {
260
268
  this.dealt = undefined;
261
269
  }
270
+ if (this.active === '' && this.childrenKnown) {
271
+ const opening = this.resolveActive(this.items);
272
+ if (opening) {
273
+ this.active = opening.value;
274
+ }
275
+ }
262
276
  }
263
277
  updated(changed) {
264
278
  super.updated?.(changed);
@@ -8,6 +8,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
8
8
  * `const row: VirtualListRenderer<Person> = (person) => person.name`.
9
9
  */
10
10
  export type VirtualListRenderer<Item = unknown> = (item: Item, index: number) => unknown;
11
+ /**
12
+ * Draws one section's header, for a list given `sectionField`, given the
13
+ * section's name and its place among the sections. Returns anything Lit
14
+ * renders.
15
+ */
16
+ export type VirtualListSectionRenderer = (name: string, index: number) => unknown;
11
17
  /** Where `scrollToIndex` puts an item, and how it gets there. */
12
18
  export interface VirtualListScrollOptions {
13
19
  /**
@@ -55,12 +61,26 @@ export interface VirtualListRange {
55
61
  * smallest item size, and follows the box as it resizes. `gap` spaces the rows, and the columns
56
62
  * of a grid, by a number of pixels the list counts in each row's place.
57
63
  *
64
+ * **Across.** `orientation="horizontal"` lays the items out along a line and
65
+ * scrolls it sideways, as a strip of cards does, windowed the same way:
66
+ * `estimate` is then an item's width, and the box needs an inline size.
67
+ *
68
+ * **Sections.** `section-field` names the field of each item that says which
69
+ * section it is in, and the list draws a header before each section's items,
70
+ * as React Aria's list box sections do. Each section is an element with the
71
+ * `group` role, named by its header, which is its first child and carries
72
+ * `data-header`, so your stylesheet styles it, such as with `position:
73
+ * sticky`. A section starts wherever the field changes, so give the items in
74
+ * order. The window counts the headers with the items, and a section the
75
+ * window starts inside is drawn with its header. `renderSection` draws a
76
+ * header of your own.
77
+ *
58
78
  * **Keyboard.** The list is one tab stop.
59
79
  *
60
80
  * | Key | What it does |
61
81
  * |---|---|
62
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. |
63
- * | 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. |
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. |
64
84
  * | Home, End | Moves to the first or the last item. |
65
85
  * | Page Down, Page Up | Moves by the number of items, or of lines in a grid, on screen. |
66
86
  * | A letter or a word | Moves to the next item whose text starts with what you typed. |
@@ -153,6 +173,7 @@ export interface VirtualListRange {
153
173
  * @csspart viewport - The scrolling box. On this tier, it needs only a block size.
154
174
  *
155
175
  * @cssstate empty - While the list has read its items and holds none.
176
+ * @cssstate horizontal - While the list runs across, with `orientation="horizontal"`.
156
177
  * @cssstate loading - While `loading` is set.
157
178
  */
158
179
  export declare class HelixHeadlessVirtualList extends HelixElement {
@@ -247,6 +268,13 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
247
268
  * follows the box as it resizes, which `columns` then does not decide.
248
269
  */
249
270
  minColumnWidth: number;
271
+ /**
272
+ * Which way the list runs. `horizontal` lays the items out along a line
273
+ * and scrolls it sideways: Left and Right move between items, the other
274
+ * way round on a right-to-left page, and `estimate` is an item's width. A
275
+ * grid always runs down.
276
+ */
277
+ orientation: 'horizontal' | 'vertical';
250
278
  /**
251
279
  * Draws one item into its row, given the item and its index. Set it as a
252
280
  * property.
@@ -256,6 +284,20 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
256
284
  * and place in the set. Where it is unset, the row `<template>` is used.
257
285
  */
258
286
  renderItem: VirtualListRenderer<any> | undefined;
287
+ /**
288
+ * Draws each section's header, given its name and its place among the
289
+ * sections, for a list given `section-field`. Set it as a property. Where
290
+ * it is unset, the header is the name as text.
291
+ */
292
+ renderSection: VirtualListSectionRenderer | undefined;
293
+ /**
294
+ * The field of each item that names its section, as a key or a path of
295
+ * keys. Set, the items are drawn in sections: a header, then the items, in
296
+ * an element with the `group` role that the header names. A section starts
297
+ * wherever the field changes, so give the items in order. A list of one
298
+ * column only.
299
+ */
300
+ sectionField: string;
259
301
  /**
260
302
  * What a press does to the items already chosen: `toggle` adds or takes out
261
303
  * the item, and `replace` selects it alone and lets the arrow keys carry the
@@ -358,6 +400,14 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
358
400
  private watchWidth;
359
401
  /** The items a span of lines holds. */
360
402
  private itemsOn;
403
+ /** The line item `at` is on: its entry in a list with sections. */
404
+ private lineOf;
405
+ /** Whether the list runs across, which a grid never does. */
406
+ private get horizontal();
407
+ /** The sections and the items they were read from, kept until either changes. */
408
+ private sectionsRead;
409
+ /** The sections `section-field` divides the items into, in a list of one column. */
410
+ private get sections();
361
411
  /**
362
412
  * Scrolls the list to show the item at `index`.
363
413
  *
@@ -443,6 +493,8 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
443
493
  private isDisabled;
444
494
  /** The item a row element drew, for an element that is one of the rows. */
445
495
  private indexOf;
496
+ /** Whether a node is one of the rows: a child, or a child of a section. */
497
+ private isRow;
446
498
  /** The row that holds the tab stop for item `index`, where it is drawn. */
447
499
  private rowFor;
448
500
  /**
@@ -541,7 +593,23 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
541
593
  * buttons are in the tab order, and those of every other row wait.
542
594
  */
543
595
  private place;
544
- /** The spacers' heights: the window's, with the line kept for focus taking its own room. */
596
+ /** The sections the rows were last placed in. */
597
+ private placedSections;
598
+ /** The function the drawn headers were drawn by, so a new one draws them again. */
599
+ private headedBy;
600
+ /** Ties each header's id to this list. */
601
+ private readonly sectionId;
602
+ /**
603
+ * The section elements a pass draws rows into, by section, for the
604
+ * sections of the wanted items: each a `group` named by its header, which
605
+ * is its first child. The ones no wanted item is in leave, and all of them
606
+ * do when the sections change.
607
+ */
608
+ private groups;
609
+ /**
610
+ * The spacers' sizes: the window's, with the line kept for focus taking its
611
+ * own room, and so do the headers drawn with their sections from outside it.
612
+ */
545
613
  private spacerSizes;
546
614
  protected render(): TemplateResult;
547
615
  }
@@ -11,6 +11,13 @@ import { holdControls, releaseControls } from '../controls.js';
11
11
  import { fillHoles, keyOf, textsOf } from '../fill.js';
12
12
  import { skipDisabled, stepIndex } from '../keys.js';
13
13
  import { drawPlan, itemsOf, stopAfter } from '../plan.js';
14
+ import { entryOf, headOf, itemsIn, sectionAt, sectionsOf } from '../sections.js';
15
+ let sectionCount = 0;
16
+ const nextSectionId = () => {
17
+ sectionCount += 1;
18
+ return `virtual-list-${sectionCount}`;
19
+ };
20
+ const ROWS = ':scope > [data-index], :scope > [data-section] > [data-index]';
14
21
  let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixElement {
15
22
  constructor() {
16
23
  super(...arguments);
@@ -26,6 +33,8 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
26
33
  this.loadingLabel = 'Loading more';
27
34
  this.measured = false;
28
35
  this.minColumnWidth = 0;
36
+ this.orientation = 'vertical';
37
+ this.sectionField = '';
29
38
  this.selectionBehavior = 'toggle';
30
39
  this.textField = '';
31
40
  this.value = '';
@@ -41,9 +50,12 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
41
50
  });
42
51
  this.virtualize = new VirtualizeController(this, {
43
52
  container: () => this.partNamed('viewport'),
44
- count: () => Math.ceil(this.data.length / this.across),
53
+ count: () => this.sections
54
+ ? this.data.length + this.sections.starts.length
55
+ : Math.ceil(this.data.length / this.across),
45
56
  estimate: () => this.estimate,
46
57
  gap: () => this.space,
58
+ horizontal: () => this.horizontal,
47
59
  measured: () => this.measured,
48
60
  rows: () => this.lines(),
49
61
  });
@@ -94,13 +106,23 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
94
106
  }
95
107
  const count = this.data.length;
96
108
  const { first, last } = this.virtualize.visible();
97
- const stepped = stepIndex(event.key, at, count, last - first - 1, {
109
+ const forward = isRtl(this) ? 'ArrowLeft' : 'ArrowRight';
110
+ const key = !this.horizontal
111
+ ? event.key
112
+ : event.key === forward
113
+ ? 'ArrowDown'
114
+ : event.key === 'ArrowLeft' || event.key === 'ArrowRight'
115
+ ? 'ArrowUp'
116
+ : event.key === 'ArrowDown' || event.key === 'ArrowUp'
117
+ ? ''
118
+ : event.key;
119
+ const stepped = stepIndex(key, at, count, last - first - 1, {
98
120
  across: this.across,
99
- forward: isRtl(this) ? 'ArrowLeft' : 'ArrowRight',
121
+ forward,
100
122
  });
101
123
  const next = stepped === undefined
102
124
  ? undefined
103
- : skipDisabled(event.key, at, stepped, count, (one) => this.isDisabled(one), this.across);
125
+ : skipDisabled(key, at, stepped, count, (one) => this.isDisabled(one), this.across);
104
126
  if (next === undefined) {
105
127
  if (stepped !== undefined) {
106
128
  event.preventDefault();
@@ -122,7 +144,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
122
144
  return;
123
145
  }
124
146
  const path = event.composedPath();
125
- const row = path.find((node) => node instanceof HTMLElement && node.parentElement === this);
147
+ const row = path.find((node) => this.isRow(node));
126
148
  const at = this.indexOf(row ?? null);
127
149
  if (at === undefined) {
128
150
  return;
@@ -143,7 +165,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
143
165
  };
144
166
  this.handleFocusIn = (event) => {
145
167
  let node = event.target;
146
- while (node && node.parentNode !== this) {
168
+ while (node && node !== this && !this.isRow(node)) {
147
169
  node = node.parentNode;
148
170
  }
149
171
  const index = this.indexOf(node);
@@ -152,6 +174,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
152
174
  }
153
175
  this.requestUpdate();
154
176
  };
177
+ this.sectionId = nextSectionId();
155
178
  }
156
179
  static { this.words = 'helix-virtual-list'; }
157
180
  get range() {
@@ -186,7 +209,32 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
186
209
  this.widthWatch.observe(viewport);
187
210
  }
188
211
  itemsOn(lines) {
189
- return itemsOf(lines, this.across, this.data.length);
212
+ const sections = this.sections;
213
+ return sections
214
+ ? itemsIn(sections, lines, this.data.length)
215
+ : itemsOf(lines, this.across, this.data.length);
216
+ }
217
+ lineOf(at) {
218
+ const sections = this.sections;
219
+ return sections ? entryOf(sections, at) : Math.floor(at / this.across);
220
+ }
221
+ get horizontal() {
222
+ return this.orientation === 'horizontal' && this.across === 1;
223
+ }
224
+ get sections() {
225
+ const data = this.data;
226
+ if (this.sectionField === '' || data.length === 0 || this.across !== 1) {
227
+ return undefined;
228
+ }
229
+ const read = this.sectionsRead;
230
+ if (read?.data !== data || read.field !== this.sectionField) {
231
+ this.sectionsRead = {
232
+ data,
233
+ field: this.sectionField,
234
+ sections: sectionsOf(data, this.sectionField),
235
+ };
236
+ }
237
+ return this.sectionsRead?.sections;
190
238
  }
191
239
  scrollToIndex(index, options = {}) {
192
240
  const count = this.data.length;
@@ -194,14 +242,14 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
194
242
  return;
195
243
  }
196
244
  const at = Math.max(0, Math.min(Math.trunc(index), count - 1));
197
- this.virtualize.reveal(Math.floor(at / this.across), 0, options);
245
+ this.virtualize.reveal(this.lineOf(at), 0, options);
198
246
  this.virtualize.measure();
199
247
  }
200
248
  get data() {
201
249
  return this.items ?? this.rows;
202
250
  }
203
251
  drawn() {
204
- return [...this.querySelectorAll(':scope > [data-index]')].map((element) => ({
252
+ return [...this.querySelectorAll(ROWS)].map((element) => ({
205
253
  element,
206
254
  index: Number(element.dataset.index),
207
255
  }));
@@ -209,6 +257,13 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
209
257
  lines() {
210
258
  const across = this.across;
211
259
  const drawn = this.drawn();
260
+ const sections = this.sections;
261
+ if (sections) {
262
+ return [
263
+ ...drawn.map(({ element, index }) => ({ element, index: entryOf(sections, index) })),
264
+ ...[...this.querySelectorAll(':scope > [data-section] > [data-entry]')].map((element) => ({ element, index: Number(element.dataset.entry) })),
265
+ ];
266
+ }
212
267
  return across === 1
213
268
  ? drawn
214
269
  : drawn
@@ -226,6 +281,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
226
281
  this.measuredAcross = across;
227
282
  this.virtualize.forget();
228
283
  }
284
+ if (this.hasUpdated && (changed.has('orientation') || changed.has('sectionField'))) {
285
+ this.virtualize.forget();
286
+ }
229
287
  if (this.hasUpdated) {
230
288
  if (changed.has('items')) {
231
289
  this.keepPlace(changed.get('items'));
@@ -245,10 +303,13 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
245
303
  if (!same(by, 0) || !same(now.length - 1, was.length - 1)) {
246
304
  return;
247
305
  }
248
- if (by % this.across === 0) {
306
+ if (this.sections) {
307
+ this.virtualize.shift(this.lineOf(by) - 1);
308
+ }
309
+ else if (by % this.across === 0) {
249
310
  this.virtualize.shift(by / this.across);
250
311
  }
251
- for (const row of this.querySelectorAll(':scope > [data-index]')) {
312
+ for (const row of this.querySelectorAll(ROWS)) {
252
313
  row.dataset.index = String(Number(row.dataset.index) + by);
253
314
  }
254
315
  this.active += by;
@@ -324,6 +385,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
324
385
  super.updated?.(changed);
325
386
  this.toggleState('empty', this.readFor !== undefined && this.data.length === 0);
326
387
  this.toggleState('loading', this.loading);
388
+ this.toggleState('horizontal', this.horizontal);
327
389
  this.watchWidth();
328
390
  const was = this.virtualize.window;
329
391
  this.virtualize.measure();
@@ -376,14 +438,18 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
376
438
  return this.disabledKeys !== '' && this.disabled.has(this.keyAt(at));
377
439
  }
378
440
  indexOf(node) {
379
- if (!(node instanceof HTMLElement) || node.parentElement !== this) {
441
+ if (!this.isRow(node)) {
380
442
  return undefined;
381
443
  }
382
444
  const index = node.dataset.index;
383
445
  return index === undefined ? undefined : Number(index);
384
446
  }
447
+ isRow(node) {
448
+ const parent = node instanceof HTMLElement ? node.parentElement : null;
449
+ return (parent === this || (parent?.parentElement === this && parent.hasAttribute('data-section')));
450
+ }
385
451
  rowFor(index) {
386
- return this.querySelector(`:scope > [data-index="${index}"]`);
452
+ return ([...this.querySelectorAll(ROWS)].find((row) => row.dataset.index === String(index)) ?? null);
387
453
  }
388
454
  keyAt(at) {
389
455
  return keyOf(this.data[at], at, this.keyField);
@@ -437,7 +503,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
437
503
  async focusIndex(index) {
438
504
  this.active = index;
439
505
  this.focusing = index;
440
- this.virtualize.reveal(Math.floor(index / this.across));
506
+ this.virtualize.reveal(this.lineOf(index));
441
507
  this.virtualize.measure();
442
508
  this.requestUpdate();
443
509
  try {
@@ -451,7 +517,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
451
517
  focusedIndex() {
452
518
  const root = this.getRootNode();
453
519
  let node = root.activeElement;
454
- while (node && node.parentElement !== this) {
520
+ while (node && node !== this && !this.isRow(node)) {
455
521
  node = node.parentElement;
456
522
  }
457
523
  const index = node instanceof HTMLElement ? node.dataset.index : undefined;
@@ -518,10 +584,20 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
518
584
  const onScreen = this.range.first;
519
585
  if (this.hasUpdated) {
520
586
  const spacers = this.spacerSizes();
521
- this.partNamed('before')?.style.setProperty('block-size', `${spacers.before}px`);
522
- this.partNamed('after')?.style.setProperty('block-size', `${spacers.after}px`);
523
- }
524
- const children = [...this.children].filter((child) => child !== template && child instanceof HTMLElement && !child.hasAttribute('slot'));
587
+ const size = this.horizontal ? 'inline-size' : 'block-size';
588
+ this.partNamed('before')?.style.setProperty(size, `${spacers.before}px`);
589
+ this.partNamed('after')?.style.setProperty(size, `${spacers.after}px`);
590
+ }
591
+ const sections = this.sections;
592
+ const regroup = sections !== this.placedSections;
593
+ this.placedSections = sections;
594
+ const children = [
595
+ ...[...this.children].filter((child) => child !== template &&
596
+ child instanceof HTMLElement &&
597
+ !child.hasAttribute('slot') &&
598
+ !child.hasAttribute('data-section')),
599
+ ...this.querySelectorAll(':scope > [data-section] > [data-index]'),
600
+ ];
525
601
  const plan = drawPlan({
526
602
  across: this.across,
527
603
  count,
@@ -529,7 +605,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
529
605
  first,
530
606
  kept: this.keptIndex(),
531
607
  last,
532
- redraw: (this.filledBy === undefined) !== (this.renderItem === undefined),
608
+ redraw: regroup || (this.filledBy === undefined) !== (this.renderItem === undefined),
533
609
  });
534
610
  const drawn = new Map();
535
611
  for (const child of children) {
@@ -541,26 +617,34 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
541
617
  }
542
618
  drawn.set(index, [...(drawn.get(index) ?? []), child]);
543
619
  }
544
- const refill = this.filledFrom !== this.data || this.filledBy !== this.renderItem;
620
+ const refill = this.filledFrom !== this.data ||
621
+ this.filledBy !== this.renderItem ||
622
+ this.headedBy !== this.renderSection;
545
623
  this.filledFrom = this.data;
546
624
  this.filledBy = this.renderItem;
625
+ this.headedBy = this.renderSection;
626
+ const groups = this.groups(sections, plan.wanted, regroup, refill);
547
627
  const fresh = new Set();
548
- let anchor = drawn.values().next().value?.[0] ?? null;
628
+ const anchors = new Map();
549
629
  for (const at of plan.wanted) {
630
+ const parent = (sections && groups.get(sectionAt(sections, at))) || this;
631
+ if (!anchors.has(parent)) {
632
+ anchors.set(parent, parent.querySelector(':scope > [data-index]'));
633
+ }
550
634
  const group = drawn.get(at);
551
635
  if (group) {
552
636
  if (refill) {
553
637
  this.fill(group, at);
554
638
  fresh.add(at);
555
639
  }
556
- anchor = group.at(-1).nextElementSibling;
640
+ anchors.set(parent, group.at(-1).nextElementSibling);
557
641
  continue;
558
642
  }
559
643
  const fragment = this.draw(template, at);
560
644
  drawn.set(at, [...fragment.children]);
561
645
  this.watchSize([...fragment.children]);
562
646
  fresh.add(at);
563
- this.insertBefore(fragment, anchor);
647
+ parent.insertBefore(fragment, anchors.get(parent) ?? null);
564
648
  }
565
649
  this.active = stopAfter(this.active, plan.wanted, {
566
650
  count,
@@ -594,13 +678,100 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
594
678
  }, 0);
595
679
  }
596
680
  }
681
+ groups(sections, wanted, regroup, refill) {
682
+ const keep = new Set(sections ? wanted.map((at) => sectionAt(sections, at)) : []);
683
+ const groups = new Map();
684
+ for (const group of this.querySelectorAll(':scope > [data-section]')) {
685
+ const at = Number(group.dataset.section);
686
+ if (regroup || !keep.has(at)) {
687
+ this.sizes?.unobserve(group.firstElementChild);
688
+ group.remove();
689
+ }
690
+ else {
691
+ groups.set(at, group);
692
+ }
693
+ }
694
+ if (!sections) {
695
+ return groups;
696
+ }
697
+ for (const at of [...keep].sort((a, b) => a - b)) {
698
+ let group = groups.get(at);
699
+ const fresh = group === undefined;
700
+ if (!group) {
701
+ group = document.createElement('div');
702
+ const header = document.createElement('div');
703
+ header.id = `${this.sectionId}-${at}`;
704
+ header.dataset.entry = String(headOf(sections, at));
705
+ header.setAttribute('data-header', '');
706
+ group.dataset.section = String(at);
707
+ group.setAttribute('role', 'group');
708
+ group.setAttribute('aria-labelledby', header.id);
709
+ group.append(header);
710
+ const after = [...groups].find(([other]) => other > at)?.[1] ?? null;
711
+ this.insertBefore(group, after);
712
+ groups.set(at, group);
713
+ this.watchSize([header]);
714
+ }
715
+ const header = group.firstElementChild;
716
+ if (fresh || refill) {
717
+ const name = sections.names[at] ?? '';
718
+ if (this.renderSection) {
719
+ render(this.renderSection(name, at), header);
720
+ }
721
+ else {
722
+ header.textContent = name;
723
+ }
724
+ }
725
+ if (this.selectionMode) {
726
+ header.setAttribute('role', 'presentation');
727
+ }
728
+ else {
729
+ header.removeAttribute('role');
730
+ }
731
+ const gap = this.space;
732
+ group.style.cssText = this.horizontal
733
+ ? `display: flex; column-gap: ${gap}px`
734
+ : gap === 0
735
+ ? ''
736
+ : `display: flex; flex-direction: column; row-gap: ${gap}px`;
737
+ }
738
+ return groups;
739
+ }
597
740
  spacerSizes() {
598
741
  const kept = this.hasUpdated ? this.keptIndex() : undefined;
599
- return this.virtualize.spacers(kept === undefined ? undefined : Math.floor(kept / this.across));
742
+ const room = this.virtualize.spacers(kept === undefined ? undefined : this.lineOf(kept));
743
+ const sections = this.sections;
744
+ if (!sections || !this.hasUpdated) {
745
+ return room;
746
+ }
747
+ const { first, last } = this.virtualize.window;
748
+ const items = this.itemsOn(this.virtualize.window);
749
+ const drawnIn = new Set();
750
+ if (items.last > items.first) {
751
+ drawnIn.add(sectionAt(sections, items.first));
752
+ }
753
+ if (kept !== undefined) {
754
+ drawnIn.add(sectionAt(sections, kept));
755
+ }
756
+ let { after, before } = room;
757
+ for (const section of drawnIn) {
758
+ const head = headOf(sections, section);
759
+ if (head < first) {
760
+ before = Math.max(0, before - this.virtualize.sizeOf(head));
761
+ }
762
+ else if (head >= last) {
763
+ after = Math.max(0, after - this.virtualize.sizeOf(head));
764
+ }
765
+ }
766
+ return { after, before };
600
767
  }
601
768
  render() {
602
769
  const { after, before } = this.spacerSizes();
603
770
  const across = this.across;
771
+ const horizontal = this.horizontal;
772
+ const size = horizontal ? 'inline-size' : 'block-size';
773
+ const flex = horizontal ? '; flex: none' : '';
774
+ const gap = this.space;
604
775
  const empty = this.readFor !== undefined &&
605
776
  !this.loading &&
606
777
  this.data.length === 0 &&
@@ -612,20 +783,24 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
612
783
  aria-multiselectable=${this.selectionMode === 'multiple' ? 'true' : nothing}
613
784
  aria-busy=${this.loading ? 'true' : nothing}
614
785
  aria-label=${this.label}
786
+ aria-orientation=${this.selectionMode && horizontal ? 'horizontal' : nothing}
787
+ style=${horizontal ? 'display: flex' : nothing}
615
788
  @scroll=${() => {
616
789
  this.virtualize.measure();
617
790
  this.report();
618
791
  }}
619
792
  >
620
- <div part="before" style="block-size: ${before}px"></div>
793
+ <div part="before" style="${size}: ${before}px${flex}"></div>
621
794
  <slot
622
- style=${across === 1
623
- ? this.space === 0
624
- ? nothing
625
- : `display: flex; flex-direction: column; row-gap: ${this.space}px; padding-block-end: ${this.space}px`
626
- : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${this.space === 0 ? '' : `; gap: ${this.space}px; padding-block-end: ${this.space}px`}`}
795
+ style=${horizontal
796
+ ? `display: flex; flex: none${gap === 0 ? '' : `; column-gap: ${gap}px; padding-inline-end: ${gap}px`}`
797
+ : across === 1
798
+ ? gap === 0
799
+ ? nothing
800
+ : `display: flex; flex-direction: column; row-gap: ${gap}px; padding-block-end: ${gap}px`
801
+ : `display: grid; grid-template-columns: repeat(${across}, minmax(0, 1fr))${gap === 0 ? '' : `; gap: ${gap}px; padding-block-end: ${gap}px`}`}
627
802
  ></slot>
628
- <div part="after" style="block-size: ${after}px"></div>
803
+ <div part="after" style="${size}: ${after}px${flex}"></div>
629
804
  ${this.loading
630
805
  ? html `<div part="loading" role=${this.selectionMode ? 'option' : 'listitem'}>
631
806
  <slot name="loading">${this.loadingLabel}</slot>
@@ -679,9 +854,18 @@ __decorate([
679
854
  __decorate([
680
855
  property({ attribute: 'min-column-width', type: Number })
681
856
  ], HelixHeadlessVirtualList.prototype, "minColumnWidth", void 0);
857
+ __decorate([
858
+ property()
859
+ ], HelixHeadlessVirtualList.prototype, "orientation", void 0);
682
860
  __decorate([
683
861
  property({ attribute: false })
684
862
  ], HelixHeadlessVirtualList.prototype, "renderItem", void 0);
863
+ __decorate([
864
+ property({ attribute: false })
865
+ ], HelixHeadlessVirtualList.prototype, "renderSection", void 0);
866
+ __decorate([
867
+ property({ attribute: 'section-field' })
868
+ ], HelixHeadlessVirtualList.prototype, "sectionField", void 0);
685
869
  __decorate([
686
870
  property({ attribute: 'selection-behavior' })
687
871
  ], HelixHeadlessVirtualList.prototype, "selectionBehavior", void 0);