@fusion.dev/helix-headless 0.166.0 → 0.168.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 (38) hide show
  1. package/custom-elements.json +2658 -481
  2. package/dist/canvas/arrange.d.ts +32 -0
  3. package/dist/canvas/arrange.js +144 -0
  4. package/dist/canvas/paste.d.ts +25 -0
  5. package/dist/canvas/paste.js +58 -0
  6. package/dist/canvas/root/canvas-root.d.ts +60 -3
  7. package/dist/canvas/root/canvas-root.js +166 -2
  8. package/dist/chat/composer/chat-composer.js +5 -1
  9. package/dist/combobox/root/combobox-root.d.ts +6 -1
  10. package/dist/combobox/root/combobox-root.js +2 -2
  11. package/dist/date-input/root/date-input-root.d.ts +1 -1
  12. package/dist/element-events.generated.js +2 -0
  13. package/dist/file-input/root/file-input-root.d.ts +1 -1
  14. package/dist/file-input/root/file-input-root.js +1 -1
  15. package/dist/format-date/root/format-date-root.d.ts +10 -3
  16. package/dist/format-date/root/format-date-root.js +6 -1
  17. package/dist/listbox/root/listbox-root.d.ts +12 -1
  18. package/dist/listbox/root/listbox-root.js +13 -1
  19. package/dist/select/root/select-root.js +1 -1
  20. package/dist/table/grid-controller.d.ts +10 -0
  21. package/dist/table/grid-controller.js +7 -0
  22. package/dist/table/root/table-root.d.ts +71 -1
  23. package/dist/table/root/table-root.js +122 -7
  24. package/dist/table/row/table-row.d.ts +17 -0
  25. package/dist/table/row/table-row.js +23 -0
  26. package/dist/time-input/root/time-input-root.d.ts +2 -1
  27. package/dist/time-input/root/time-input-root.js +1 -1
  28. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  29. package/dist/time-picker/root/time-picker-root.js +79 -21
  30. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  31. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  32. package/dist/virtual-list/sections.d.ts +27 -0
  33. package/dist/virtual-list/sections.js +43 -0
  34. package/lint.json +1 -1
  35. package/package.json +5 -5
  36. package/types/jsx.d.ts +454 -76
  37. package/types/vue.d.ts +89 -7
  38. package/types/words.d.ts +27 -0
@@ -1,7 +1,7 @@
1
1
  import { AriaForwardController, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
- import '../../tag/index.js';
4
+ import '../../tag/root/index.js';
5
5
  declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
6
6
  /**
7
7
  * @summary A text field that filters a list of options as you type.
@@ -668,6 +668,11 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
668
668
  * slot is for the page and the method is for whoever is building the styled
669
669
  * layer, and a fallback with only the first is one a tier can restyle and
670
670
  * cannot change.
671
+ *
672
+ * **One em square by its own attributes**, which any stylesheet overrides.
673
+ * An inline `<svg>` with a `viewBox` and no size fills the width it is given:
674
+ * measured with no stylesheet, the caret was 1,264px tall, the field 1,289px,
675
+ * and the panel opened above the top of the page.
671
676
  */
672
677
  protected renderIndicator(): TemplateResult;
673
678
  /**
@@ -13,7 +13,7 @@ import { html } from 'lit/static-html.js';
13
13
  import { optionsAmong } from '../../option/root/option-root.js';
14
14
  import { HelixHeadlessTag } from '../../tag/root/tag-root.js';
15
15
  import '../../listbox/index.js';
16
- import '../../tag/index.js';
16
+ import '../../tag/root/index.js';
17
17
  const isInsertion = (event) => !event.inputType?.startsWith('delete');
18
18
  let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
19
19
  constructor() {
@@ -612,7 +612,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
612
612
  }
613
613
  renderIndicator() {
614
614
  return html `<slot name="indicator">
615
- <svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">
615
+ <svg part="indicator" fill="currentColor" width="1em" height="1em" viewBox=${caretDown.viewBox} aria-hidden="true">
616
616
  ${unsafeSVG(caretDown.content)}
617
617
  </svg>
618
618
  </slot>`;
@@ -51,7 +51,7 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
51
51
  * | Page Up, Page Down on a segment | Steps by a page: a week of days, two months, or five years. |
52
52
  * | Home, End on a segment | Goes to the segment's first or last value. |
53
53
  * | Digits on a segment | Fill it, and move to the next once it can take no more. |
54
- * | Backspace, Delete on a segment | Empty the segment. |
54
+ * | Backspace, Delete on a segment | Take off its last digit, or move to the segment before where it is empty. |
55
55
  * | Left, Right on a segment | Move to the previous or the next segment, along the line of text. |
56
56
  *
57
57
  * In the calendar, the keys are `DatePicker`'s: the arrows move by a day or a
@@ -17,6 +17,7 @@ registerElementEvents({
17
17
  'helix-headless-callout': ['helix-dismiss'],
18
18
  'helix-headless-canvas': [
19
19
  'helix-before-move',
20
+ 'helix-before-paste',
20
21
  'helix-before-reconnect',
21
22
  'helix-before-relabel',
22
23
  'helix-canvas-edit',
@@ -25,6 +26,7 @@ registerElementEvents({
25
26
  'helix-canvas-zoom',
26
27
  'helix-connect',
27
28
  'helix-move',
29
+ 'helix-pasted',
28
30
  'helix-reconnect',
29
31
  'helix-relabel',
30
32
  'helix-remove',
@@ -2,7 +2,7 @@ import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core'
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { type FileStatus } from '../status.js';
4
4
  export type { FileStatus } from '../status.js';
5
- import '../../tag/index.js';
5
+ import '../../tag/root/index.js';
6
6
  declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
7
7
  /**
8
8
  * @summary A field for choosing files from a picker or by dropping them on it.
@@ -11,7 +11,7 @@ import { property, state } from 'lit/decorators.js';
11
11
  import { html } from 'lit/static-html.js';
12
12
  import { screenFiles } from '../screen.js';
13
13
  import { barOf, saysStatus } from '../status.js';
14
- import '../../tag/index.js';
14
+ import '../../tag/root/index.js';
15
15
  const size = (bytes, locale, inBytes) => {
16
16
  const units = ['', 'KB', 'MB', 'GB', 'TB'];
17
17
  let value = bytes;
@@ -84,7 +84,8 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
84
84
  */
85
85
  calendar?: DateCalendar | MonthCalendar;
86
86
  /**
87
- * The timestamp, in any format `Date` can parse.
87
+ * The timestamp: a `Date`, milliseconds since the epoch, or a string in any
88
+ * format `Date` can parse. The attribute is the string.
88
89
  *
89
90
  * An ISO string is what an API sends and what `<time datetime>` takes, so it
90
91
  * is the shape that needs no translation at either end. Left unset this
@@ -100,7 +101,7 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
100
101
  * it. An element rendered on a server needs the attribute; one rendered in a
101
102
  * browser may use either.
102
103
  */
103
- date: string;
104
+ date: Date | number | string;
104
105
  /**
105
106
  * The locale's own pattern for the date: `full`, `long`, `medium`, or
106
107
  * `short`. Set, it stands in for `weekday`, `day`, `month`, `year` and `era`.
@@ -259,7 +260,13 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
259
260
  private get dateOnly();
260
261
  /** The timestamp, or nothing where there is not one to read. */
261
262
  private get moment();
262
- /** The timestamp as written, in the attribute or in the light DOM. */
263
+ /**
264
+ * The timestamp as written, in the property, the attribute or the light DOM.
265
+ *
266
+ * A `Date` or a number is written as its ISO string, so it is read the way an
267
+ * attribute is. Measured before this: assigning `new Date()` threw, since
268
+ * only a string has `trim`, where Shoelace and Web Awesome take a `Date`.
269
+ */
263
270
  private get source();
264
271
  /** Whether the reading is how long ago rather than the date. */
265
272
  private get reading();
@@ -44,7 +44,12 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
44
44
  return Number.isNaN(parsed.getTime()) ? undefined : parsed;
45
45
  }
46
46
  get source() {
47
- return (this.date || this.written).trim();
47
+ const date = this.date;
48
+ if (typeof date === 'string') {
49
+ return (date || this.written).trim();
50
+ }
51
+ const at = new Date(date);
52
+ return Number.isNaN(at.getTime()) ? '' : at.toISOString();
48
53
  }
49
54
  get reading() {
50
55
  return this.elapsed || this.relative !== undefined;
@@ -542,7 +542,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
542
542
  */
543
543
  navigate(event: KeyboardEvent): void;
544
544
  /**
545
- * Scrolls the chosen option to the middle of the list.
545
+ * Scrolls the chosen option to the middle of the list, or the highlighted one
546
+ * where nothing is chosen.
546
547
  *
547
548
  * A method rather than something this does on its own, because a listbox
548
549
  * opening on its answer usually wants it merely *visible* and the browser
@@ -554,6 +555,16 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
554
555
  * arithmetic, since the chosen option may not be drawn.
555
556
  */
556
557
  reveal(): void;
558
+ /**
559
+ * Puts the highlight on the option holding `value`, without choosing it, so
560
+ * the arrow keys move from there. Nothing changes where no option a reader
561
+ * can reach holds `value`.
562
+ *
563
+ * Time Picker calls it for a time between two of its steps, which no option
564
+ * holds: without it the first arrow moved from the top of the list, so Down
565
+ * from 14:07 went to midnight.
566
+ */
567
+ highlightValue(value: string): void;
557
568
  /** Hides what the filter excludes, and writes the rest onto the options. */
558
569
  private place;
559
570
  /**
@@ -391,11 +391,23 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
391
391
  return;
392
392
  }
393
393
  const items = this.partNamed('items');
394
- const chosen = this.options.find((option) => option.selected && !option.hidden);
394
+ const chosen = this.options.find((option) => option.selected && !option.hidden) ??
395
+ this.options.find((option) => option.value === this.activeValue && !option.hidden);
395
396
  if (items && chosen) {
396
397
  centerInView(items, chosen);
397
398
  }
398
399
  }
400
+ highlightValue(value) {
401
+ if (!this.shown.some((option) => !option.disabled && option.value === value)) {
402
+ return;
403
+ }
404
+ this.typeahead.reset();
405
+ this.parked = false;
406
+ this.activeValue = value;
407
+ if (this.childrenKnown) {
408
+ this.place();
409
+ }
410
+ }
399
411
  place() {
400
412
  this.forgetRemoved();
401
413
  const typed = this.filter.trim();
@@ -340,7 +340,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
340
340
  }
341
341
  renderIndicator() {
342
342
  return html `<slot name="indicator">
343
- <svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">
343
+ <svg part="indicator" fill="currentColor" width="1em" height="1em" viewBox=${caretDown.viewBox} aria-hidden="true">
344
344
  ${unsafeSVG(caretDown.content)}
345
345
  </svg>
346
346
  </slot>`;
@@ -51,6 +51,16 @@ export interface TableGridOptions {
51
51
  disclose?: (row: number, forward: boolean) => boolean | number;
52
52
  /** Whether the table is a grid now. */
53
53
  on: () => boolean;
54
+ /**
55
+ * The rows drawn now, in a table that draws only some of them, which are
56
+ * the only rows whose cells are marked. Every row where it is left out.
57
+ */
58
+ drawn?: () => ReadonlySet<Element> | undefined;
59
+ /**
60
+ * Brings drawn row `row` into view before a key focuses it, in a table that
61
+ * draws only the rows in view, so the row is drawn by then.
62
+ */
63
+ reveal?: (row: number) => void;
54
64
  }
55
65
  /**
56
66
  * The keyboard of a `grid` table: one tab stop among its cells, the arrow
@@ -247,6 +247,9 @@ export class TableGridController {
247
247
  this.stopRow = place.row === -1 ? 'head' : this.options.rows()[place.row];
248
248
  this.stopColumn = place.column;
249
249
  this.focusWanted = focus;
250
+ if (focus && place.row >= 0) {
251
+ this.options.reveal?.(place.row);
252
+ }
250
253
  this.host.requestUpdate();
251
254
  }
252
255
  mark() {
@@ -259,7 +262,11 @@ export class TableGridController {
259
262
  column.gridCell = on;
260
263
  column.tabStop = on && at.row === -1 && at.column === index + this.offset;
261
264
  }
265
+ const drawn = this.options.drawn?.();
262
266
  for (const [index, line] of this.options.rows().entries()) {
267
+ if (drawn && !drawn.has(line)) {
268
+ continue;
269
+ }
263
270
  const row = line;
264
271
  if (row.isTableRow) {
265
272
  const here = on && at.row === index;
@@ -36,6 +36,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
36
36
  * The `heading` slot names the table: slot a heading or a line of text into
37
37
  * it, and the table is named by its words. An `aria-label` or `aria-labelledby` on the
38
38
  * host wins over it.
39
+ * A table with no name is reported once in a development build.
39
40
  *
40
41
  * **Selecting.** `selectable` adds a column of checkboxes.
41
42
  *
@@ -121,9 +122,17 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
121
122
  * | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
122
123
  * | Tab | Leaves the table. |
123
124
  *
125
+ * **Long tables.** `windowed` draws only the rows in view, as Data Table's
126
+ * does, and the box of the table scrolls under headings that stick. The
127
+ * table still says how many rows it has with `aria-rowcount`, and each row
128
+ * its place with `aria-rowindex`. The keys reach every row, drawn or not, and
129
+ * the row holding focus stays drawn when it scrolls away. Nested rows,
130
+ * sorting, and selecting work as they do in a table that draws every row.
131
+ *
124
132
  * **Loading more.** `load-more` sends `helix-load-more` once the end of the
125
133
  * table comes within a screen of view, in the box that scrolls it or the
126
- * page, once for each number of rows: add rows and it can send it again. Set
134
+ * page, or in the table's own box when it is `windowed`, once for each
135
+ * number of rows: add rows and it can send it again. Set
127
136
  * `loading` while you fetch, and the table asks for nothing until it clears.
128
137
  *
129
138
  * When the rows did not load, set `error-text`, such as "The orders did not
@@ -158,6 +167,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
158
167
  * @event {HelixSortedEvent} helix-sorted - Fired after the sort order changes, with the new order.
159
168
  * @event {HelixToggleEvent} helix-toggle - Fired from a row once the rows under it are shown or hidden, with whether it is open in `detail.checked`.
160
169
  *
170
+ * @csspart after - The space that stands in for the rows below the window. Only while `windowed`.
171
+ * @csspart before - The space that stands in for the rows above the window. Only while `windowed`.
161
172
  * @csspart checkbox - The checkbox in the heading that selects every row, in a `selectable` table.
162
173
  * @csspart checkbox-box - The box drawn for the checkbox's input.
163
174
  * @csspart checkbox-control - The checkbox's input.
@@ -399,11 +410,35 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
399
410
  sortedLabel: string;
400
411
  /** What screen readers hear when the table stops being sorted. */
401
412
  unsortedLabel: string;
413
+ /**
414
+ * Draws only the rows in view, with empty space in place of the rest, as
415
+ * Data Table's `windowed` does. The table's box then scrolls, at the
416
+ * height `--table-block-size` gives it in the styled tier, and the
417
+ * headings stick to its top.
418
+ *
419
+ * Off by default. Windowing costs a scroll listener, a measurement per row
420
+ * drawn, and a height on the table, and a table of twenty rows should pay
421
+ * none of them.
422
+ */
423
+ windowed: boolean;
402
424
  private sortState;
403
425
  private selectedState;
404
426
  private expandedState;
405
427
  /** Whether any row sits under another, which makes the table a tree. */
406
428
  private get nests();
429
+ /**
430
+ * What the table read from its rows, kept until anything asks for an
431
+ * update or the turn ends.
432
+ *
433
+ * **Read once rather than on every question.** A render, the assignment
434
+ * and the keyboard each asked for the rows in order dozens of times, and
435
+ * each answer walked every row: in Chromium a windowed table of 3,000 rows
436
+ * took 89 milliseconds to move its window, 42 of them reading the keys.
437
+ */
438
+ private memo;
439
+ private remember;
440
+ /** @internal Forgets what was read from the rows, then asks for an update as Lit does. */
441
+ requestUpdate(name?: PropertyKey, oldValue?: unknown, options?: Parameters<HelixElement['requestUpdate']>[2]): void;
407
442
  /** The keys of the rows whose rows are shown. */
408
443
  private get open();
409
444
  /**
@@ -424,6 +459,37 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
424
459
  protected readonly aria: AriaForwardController;
425
460
  /** The keyboard of a `grid` table, in `grid-controller.ts`. */
426
461
  private readonly gridKeys;
462
+ /**
463
+ * The slice of a long table that is drawn, under `windowed`. The rows are
464
+ * the page's elements, so the window is which of them the body slot is
465
+ * given: a row it is not given is not laid out and not in the
466
+ * accessibility tree.
467
+ */
468
+ private readonly virtualize;
469
+ /** How many rows are drawn in order, read when the rows are assigned. */
470
+ private arrangedCount;
471
+ /** A `windowed` table has rows while it holds any, drawn or not. */
472
+ protected get hasRows(): boolean;
473
+ /** The rows the body slot was last given, under `windowed`. */
474
+ private drawnRows;
475
+ /**
476
+ * The rows drawn now, each by its first cell, with its place among the
477
+ * rows. A row is `display: contents` and has no box, and a cell is a grid
478
+ * item stretched to its row, as Data Table measures its rows.
479
+ */
480
+ private measuredRows;
481
+ /** The row outside the window that stays drawn: the grid's stop, or the row holding focus. */
482
+ private keptRow;
483
+ /**
484
+ * The rows to draw under `windowed`: the window, and the row kept for
485
+ * focus beside it on its side.
486
+ */
487
+ private windowOf;
488
+ /**
489
+ * Scrolls drawn row `row` into view below the headings, and moves the
490
+ * window to it, before a key focuses it.
491
+ */
492
+ private revealRow;
427
493
  /**
428
494
  * Moving rows by their grips, in a `reorderable` table. The rows are the
429
495
  * page's, so a drop asks the page to move them, and the grips do nothing
@@ -459,6 +525,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
459
525
  * which is what a framework holding those rows needs.
460
526
  */
461
527
  private get arrangedRows();
528
+ /** The rows in the order they are drawn, worked out again. */
529
+ private arrange;
462
530
  /**
463
531
  * The rows of a tree in the order they are drawn: each row followed by the
464
532
  * rows under it while it is open, at every depth, keeping the order `rows`
@@ -714,6 +782,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
714
782
  * are asked for before a reader reaches the last. List does the same.
715
783
  */
716
784
  private watchEnd;
785
+ /** Asks for more once a reader of a `windowed` table is within a screen of its end. */
786
+ private readonly nearEnd;
717
787
  /** Whether the end of the table was in reach when last measured. */
718
788
  private endInView;
719
789
  /** Sends `helix-load-more`, once for each number of rows, and never while loading. */
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, assignSlots, auditName, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
@@ -46,6 +46,7 @@ const lengthOf = (element) => textOf(element).length;
46
46
  const numberOf = (value) => Number(value.replaceAll(',', ''));
47
47
  const STATUS = new Set(['empty', 'error', 'loading']);
48
48
  const NAMED = new Set([...STATUS, 'heading']);
49
+ const ESTIMATE = 40;
49
50
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
50
51
  constructor() {
51
52
  super(...arguments);
@@ -78,9 +79,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
78
79
  this.selectRowLabel = 'Select {row}';
79
80
  this.sortedLabel = '{direction, select, ascending {Sorted by {column}, ascending} descending {Sorted by {column}, descending} other {Sorted by {column}}}';
80
81
  this.unsortedLabel = 'Not sorted';
82
+ this.windowed = false;
81
83
  this.sortState = undefined;
82
84
  this.selectedState = undefined;
83
85
  this.expandedState = undefined;
86
+ this.memo = {};
84
87
  this.headed = false;
85
88
  this.aria = new AriaForwardController(this, {
86
89
  label: true,
@@ -95,9 +98,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
95
98
  },
96
99
  columns: () => this.tableColumns.filter((column) => column.shown),
97
100
  disclose: (at, forward) => this.disclose(at, forward),
101
+ drawn: () => (this.windowed ? new Set(this.drawnRows) : undefined),
98
102
  lead: () => this.lead,
99
103
  on: () => this.grid,
100
104
  part: (name) => this.partNamed(name),
105
+ reveal: (row) => this.revealRow(row),
101
106
  rows: () => (this.childrenKnown ? this.arrangedRows : []),
102
107
  rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
103
108
  selectAll: () => this.selectable &&
@@ -105,6 +110,15 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
105
110
  this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
106
111
  selectRange: (to, from) => this.selectRange(to, from),
107
112
  });
113
+ this.virtualize = new VirtualizeController(this, {
114
+ container: () => (this.windowed ? this.partNamed('table') : null),
115
+ count: () => this.arrangedCount,
116
+ estimate: () => ESTIMATE,
117
+ measured: () => this.windowed,
118
+ rows: () => (this.windowed ? this.measuredRows() : []),
119
+ });
120
+ this.arrangedCount = 0;
121
+ this.drawnRows = [];
108
122
  this.reorder = new ReorderController(this, {
109
123
  axis: 'block',
110
124
  box: (key) => this.gripOf(this.rowFor(key))?.getBoundingClientRect(),
@@ -191,6 +205,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
191
205
  this.watcher = undefined;
192
206
  this.alignment = undefined;
193
207
  this.aligned = '';
208
+ this.nearEnd = () => {
209
+ if (this.windowed && this.loadMore && this.virtualize.endInSight(this.arrangedCount)) {
210
+ this.askForMore();
211
+ }
212
+ };
194
213
  this.endInView = false;
195
214
  }
196
215
  static { this.words = 'helix-table'; }
@@ -219,7 +238,23 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
219
238
  this.requestUpdate('sort', previous);
220
239
  }
221
240
  get nests() {
222
- return this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== '');
241
+ return this.remember('nests', () => this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== ''));
242
+ }
243
+ remember(key, compute) {
244
+ const memo = this.memo;
245
+ if (Object.keys(memo).length === 0) {
246
+ queueMicrotask(() => {
247
+ if (this.memo === memo) {
248
+ this.memo = {};
249
+ }
250
+ });
251
+ }
252
+ memo[key] ??= compute();
253
+ return memo[key];
254
+ }
255
+ requestUpdate(name, oldValue, options) {
256
+ this.memo = {};
257
+ super.requestUpdate(name, oldValue, options);
223
258
  }
224
259
  get open() {
225
260
  return new Set(commaList(this.expanded));
@@ -251,6 +286,44 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
251
286
  row.dispatchEvent(new HelixToggleEvent({ checked: next }));
252
287
  }
253
288
  }
289
+ get hasRows() {
290
+ return (this.windowed && this.arrangedCount > 0) || super.hasRows;
291
+ }
292
+ measuredRows() {
293
+ const rows = this.arrangedRows;
294
+ const at = new Map(rows.map((row, index) => [row, index]));
295
+ return this.drawnRows.flatMap((row) => {
296
+ const cell = [...row.children].find((child) => child.getAttribute('hidden') === null && child.getAttribute('slot') === null);
297
+ const index = at.get(row);
298
+ return cell instanceof HTMLElement && index !== undefined ? [{ element: cell, index }] : [];
299
+ });
300
+ }
301
+ keptRow(rows) {
302
+ const { first, last } = this.virtualize.window;
303
+ const active = this.getRootNode().activeElement;
304
+ const focused = this.grid
305
+ ? this.gridKeys.current().row
306
+ : rows.findIndex((row) => active !== null && row.contains(active));
307
+ return focused >= 0 && (focused < first || focused >= last) ? focused : undefined;
308
+ }
309
+ windowOf(rows) {
310
+ const { first, last } = this.virtualize.window;
311
+ const drawn = rows.slice(first, last);
312
+ const kept = this.keptRow(rows);
313
+ const row = kept === undefined ? undefined : rows[kept];
314
+ if (kept !== undefined && row) {
315
+ return kept < first ? [row, ...drawn] : [...drawn, row];
316
+ }
317
+ return drawn;
318
+ }
319
+ revealRow(row) {
320
+ if (!this.windowed) {
321
+ return;
322
+ }
323
+ const heading = this.gridKeys.cellAt({ column: 0, row: -1 });
324
+ this.virtualize.reveal(row, heading?.getBoundingClientRect().height ?? 0);
325
+ this.virtualize.measure();
326
+ }
254
327
  get lead() {
255
328
  return [
256
329
  ...(this.reorderable ? ['grip'] : []),
@@ -282,6 +355,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
282
355
  return this.requestSort(format(cycle(parse(this.sort), field)));
283
356
  }
284
357
  get arrangedRows() {
358
+ return this.remember('arranged', () => this.arrange());
359
+ }
360
+ arrange() {
285
361
  const rows = this.slotted;
286
362
  const spec = parse(this.sort);
287
363
  const declared = this.tableColumns;
@@ -381,10 +457,10 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
381
457
  });
382
458
  }
383
459
  get keyed() {
384
- return this.slotted.map((row, index) => ({
460
+ return this.remember('keyed', () => this.slotted.map((row, index) => ({
385
461
  key: row.value || String(index),
386
462
  row: row,
387
- }));
463
+ })));
388
464
  }
389
465
  get chosen() {
390
466
  return new Set(commaList(this.selected));
@@ -538,10 +614,26 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
538
614
  const tree = this.nests;
539
615
  const shapes = tree ? this.shape() : undefined;
540
616
  const open = this.open;
617
+ const drawn = this.windowed ? new Set(this.drawnRows) : undefined;
618
+ const places = drawn
619
+ ? new Map(this.arrangedRows.map((row, index) => [row, index + 2]))
620
+ : undefined;
541
621
  for (const { key, row } of this.keyed) {
542
622
  if (!row.isTableRow) {
543
623
  continue;
544
624
  }
625
+ if (drawn && !drawn.has(row)) {
626
+ const shape = shapes?.get(key);
627
+ whenHydrated(row, () => {
628
+ row.selectable = this.selectable;
629
+ row.gripLabel = this.reorderable ? row.gripLabel || ' ' : '';
630
+ row.level = shape?.level ?? 0;
631
+ row.branch = shape?.branch ?? false;
632
+ row.open = open.has(key);
633
+ });
634
+ continue;
635
+ }
636
+ const place = places?.get(row) ?? 0;
545
637
  const name = textOf(row.children[0]) || key;
546
638
  const label = formatWords(this, 'selectRowLabel', { row: name });
547
639
  const grip = this.reorderable ? formatWords(this, 'moveRowLabel', { row: name }) : '';
@@ -560,6 +652,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
560
652
  row.open = open.has(key);
561
653
  row.expandLabel = expand;
562
654
  row.expandWith = tree ? () => this.toggleRow(key) : undefined;
655
+ row.rowIndex = place;
563
656
  });
564
657
  }
565
658
  }
@@ -676,7 +769,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
676
769
  const columns = new Set(this.tableColumns);
677
770
  const headings = new Set(this.headings);
678
771
  const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
679
- assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...this.arrangedRows], (child) => columns.has(child)
772
+ const arranged = this.arrangedRows;
773
+ this.arrangedCount = arranged.length;
774
+ const rows = this.windowed ? this.windowOf(arranged) : arranged;
775
+ this.drawnRows = this.windowed ? rows : [];
776
+ assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...rows], (child) => columns.has(child)
680
777
  ? 'columns'
681
778
  : headings.has(child)
682
779
  ? 'heading'
@@ -719,14 +816,22 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
719
816
  this.markSorted();
720
817
  this.markSelected();
721
818
  this.nameByHeading();
819
+ auditName(this, () => null, 'Give it a `label`, or put a heading in its `heading` slot, that says what the rows are.', () => this.label !== '' || this.headings.length > 0);
722
820
  if (this.childrenKnown) {
723
821
  this.gridKeys.mark();
724
822
  this.gridKeys.place();
725
823
  }
824
+ if (this.windowed) {
825
+ this.virtualize.measure();
826
+ this.virtualize.settle();
827
+ }
726
828
  this.watchEnd();
727
829
  }
728
830
  watchEnd() {
729
- const end = this.loadMore ? this.renderRoot.querySelector('[data-end]') : null;
831
+ if (this.windowed) {
832
+ this.nearEnd();
833
+ }
834
+ const end = this.loadMore && !this.windowed ? this.renderRoot.querySelector('[data-end]') : null;
730
835
  if (end === this.watchedEnd) {
731
836
  if (this.endInView) {
732
837
  this.askForMore();
@@ -810,6 +915,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
810
915
  render() {
811
916
  const template = this.template;
812
917
  const grid = this.grid;
918
+ const windowed = this.windowed && this.childrenKnown;
919
+ const count = windowed ? this.arrangedRows.length : 0;
920
+ const room = windowed ? this.virtualize.spacers(this.keptRow(this.arrangedRows)) : undefined;
813
921
  const start = grid ? html `<span part="grid-start" tabindex="-1"></span>` : nothing;
814
922
  const end = grid ? html `<span part="grid-end" tabindex="-1"></span>` : nothing;
815
923
  return html `<slot name="heading"></slot>${start}<div
@@ -818,15 +926,19 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
818
926
  aria-multiselectable=${grid && this.selectable ? (this.single ? 'false' : 'true') : nothing}
819
927
  aria-busy=${this.loading ? 'true' : nothing}
820
928
  aria-label=${this.label || this.fallbackLabel}
929
+ aria-rowcount=${windowed ? count + 1 : nothing}
821
930
  style=${template ? `--table-template: ${template}` : ''}
931
+ @scroll=${this.nearEnd}
822
932
  >
823
- <div part="head" role="row">${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
933
+ <div part="head" role="row" aria-rowindex=${windowed ? 1 : nothing}>${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
934
+ ${room ? html `<div part="before" role="presentation" style="block-size: ${room.before}px"></div>` : nothing}
824
935
  <slot
825
936
  @slotchange=${(event) => {
826
937
  this.countRows(event);
827
938
  this.handleSlotChange();
828
939
  }}
829
940
  ></slot>
941
+ ${room ? html `<div part="after" role="presentation" style="block-size: ${room.after}px"></div>` : nothing}
830
942
  ${this.errorText === ''
831
943
  ? this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')
832
944
  : html `<p part="error" role="row"><span role=${grid ? 'gridcell' : 'cell'}><slot name="error">${this.errorText}</slot></span></p>`}
@@ -948,6 +1060,9 @@ __decorate([
948
1060
  words,
949
1061
  property({ attribute: 'unsorted-label' })
950
1062
  ], HelixHeadlessTable.prototype, "unsortedLabel", void 0);
1063
+ __decorate([
1064
+ property({ reflect: true, type: Boolean })
1065
+ ], HelixHeadlessTable.prototype, "windowed", void 0);
951
1066
  __decorate([
952
1067
  state()
953
1068
  ], HelixHeadlessTable.prototype, "sortState", void 0);