@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
@@ -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, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, filterKeeps, focusTarget, HelixElement, HelixFilterEvent, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, markMatches, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -33,8 +33,12 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
33
33
  this.backLabel = 'Back';
34
34
  this.closeOnSelect = true;
35
35
  this.context = false;
36
+ this.filter = '';
37
+ this.filterable = false;
38
+ this.filterLabel = 'Filter';
36
39
  this.label = '';
37
40
  this.longPressLabel = 'Long press or press Alt+Down to show the menu';
41
+ this.noResultsText = 'No matches';
38
42
  this.offset = 4;
39
43
  this.longPress = new LongPress(this, {
40
44
  onLongPress: () => {
@@ -68,7 +72,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
68
72
  });
69
73
  this.aria = new AriaForwardController(this, {
70
74
  label: true,
71
- target: () => this.partNamed('surface'),
75
+ target: () => this.menuBox,
72
76
  });
73
77
  this.panel = new PopoverController(this, {
74
78
  anchor: () => this.context && this.point
@@ -89,6 +93,11 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
89
93
  surface: () => this.partNamed('surface'),
90
94
  });
91
95
  this.settledOnce = false;
96
+ this.filterPage = null;
97
+ this.handleFilterInput = (event) => {
98
+ event.stopPropagation();
99
+ this.filterBy(event.target.value);
100
+ };
92
101
  this.handleContextMenu = (event) => {
93
102
  if (!this.context) {
94
103
  return;
@@ -129,14 +138,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
129
138
  };
130
139
  this.handleFocusIn = (event) => {
131
140
  const item = this.items.find((row) => row.contains(event.target));
132
- if (!item) {
133
- return;
141
+ if (item) {
142
+ this.revealRow(item);
134
143
  }
135
- const holder = this.flies ? this.holderOf(item) : null;
136
- const surface = holder
137
- ? holder.shadowRoot?.querySelector('[part="surface"]')
138
- : this.partNamed('surface');
139
- scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
140
144
  };
141
145
  this.handleKeydown = (event) => {
142
146
  if (!this.open) {
@@ -146,9 +150,23 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
146
150
  const items = this.items;
147
151
  const at = items.findIndex((item) => item.active);
148
152
  const along = inlineStep(this, event.key);
153
+ const typing = this.filterable && event.composedPath()[0] === this.field;
154
+ const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
149
155
  if (event.key === 'Escape') {
150
156
  event.stopPropagation();
151
- this.leave();
157
+ if (!this.filterable || this.filter === '') {
158
+ this.leave();
159
+ }
160
+ else {
161
+ this.filterBy('');
162
+ }
163
+ }
164
+ else if (typing && step !== 0) {
165
+ const count = items.length;
166
+ this.reach(items[at < 0 ? (step > 0 ? 0 : count - 1) : (at + step + count) % count]);
167
+ }
168
+ else if (typing && event.key === 'Enter') {
169
+ items[at]?.renderRoot.querySelector('[part="item"]')?.click();
152
170
  }
153
171
  else if (along === 1 && items[at]?.hasSubmenu) {
154
172
  this.enter(items[at]);
@@ -188,6 +206,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
188
206
  this.longPress.down(event);
189
207
  }
190
208
  };
209
+ this.handleMouseDown = (event) => {
210
+ if (this.filterable &&
211
+ this.open &&
212
+ event.composedPath()[0] !== this.field &&
213
+ !this.fromTrigger(event)) {
214
+ event.preventDefault();
215
+ }
216
+ };
191
217
  this.handlePointerMove = (event) => {
192
218
  this.longPress.move(event);
193
219
  };
@@ -209,7 +235,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
209
235
  }
210
236
  };
211
237
  this.handleHover = (event) => {
212
- if (!this.flies || !this.open || event.pointerType !== 'mouse') {
238
+ if (!(this.flies || this.filterable) || !this.open || event.pointerType !== 'mouse') {
213
239
  return;
214
240
  }
215
241
  const row = event
@@ -221,7 +247,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
221
247
  this.hovered = row;
222
248
  this.grace = undefined;
223
249
  if (!row.disabled) {
224
- row.focus();
250
+ this.reach(row);
251
+ }
252
+ if (!this.flies) {
253
+ return;
225
254
  }
226
255
  clearTimeout(this.hoverTimer);
227
256
  this.hoverTimer = setTimeout(() => {
@@ -334,7 +363,53 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
334
363
  }
335
364
  get items() {
336
365
  const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
337
- return rows.filter((row) => !row.disabled);
366
+ return rows.filter((row) => !row.disabled && this.kept(row));
367
+ }
368
+ get menuBox() {
369
+ return this.filterable
370
+ ? (this.renderRoot?.querySelector('#items') ?? null)
371
+ : this.partNamed('surface');
372
+ }
373
+ get field() {
374
+ return this.partNamed('search-field');
375
+ }
376
+ get filtering() {
377
+ return this.filterable && this.filter.trim() !== '' && this.filter !== this.refused;
378
+ }
379
+ kept(row) {
380
+ return (!this.filtering ||
381
+ this.holderOf(row) !== this.filterPage ||
382
+ filterKeeps(slottedWords(row, ['']), this.filter.trim(), this.filterMatch));
383
+ }
384
+ requestFilter(value) {
385
+ if (value === this.filter) {
386
+ return;
387
+ }
388
+ this.filterPage = this.flies ? this.keyPage : this.drilldown.page;
389
+ this.refused = this.dispatchEvent(new HelixFilterEvent({ value })) ? undefined : value;
390
+ this.filter = value;
391
+ }
392
+ filterBy(value) {
393
+ this.requestFilter(value);
394
+ void this.updateComplete.then(() => this.reach(this.items[0]));
395
+ }
396
+ reach(row) {
397
+ if (!this.filterable) {
398
+ row?.focus();
399
+ return;
400
+ }
401
+ if (this.highlighted && this.highlighted !== row) {
402
+ this.highlighted.highlighted = false;
403
+ }
404
+ this.highlighted = row;
405
+ const field = this.field;
406
+ if (field && 'ariaActiveDescendantElement' in field) {
407
+ field.ariaActiveDescendantElement = row ?? null;
408
+ }
409
+ if (row) {
410
+ row.highlighted = true;
411
+ this.revealRow(row);
412
+ }
338
413
  }
339
414
  show() {
340
415
  this.openAt('first');
@@ -345,7 +420,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
345
420
  }
346
421
  this.updateComplete.then(() => {
347
422
  const items = this.items;
348
- (end === 'first' ? items[0] : items[items.length - 1])?.focus();
423
+ this.field?.focus();
424
+ this.reach(end === 'first' ? items[0] : items[items.length - 1]);
349
425
  });
350
426
  }
351
427
  hide() {
@@ -355,6 +431,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
355
431
  this.panel.hide();
356
432
  this.drilldown.reset();
357
433
  this.stopHover();
434
+ this.requestFilter('');
435
+ this.reach(undefined);
358
436
  if (deepContains(this, deepActiveElement())) {
359
437
  ;
360
438
  (this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
@@ -377,18 +455,20 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
377
455
  if (!this.drilldown.enter(item)) {
378
456
  return;
379
457
  }
458
+ this.requestFilter('');
380
459
  void this.updateComplete.then(async () => {
381
460
  if (!this.flies) {
382
- this.items[0]?.focusItem();
461
+ this.reach(this.items[0]);
383
462
  return;
384
463
  }
385
464
  await item.updateComplete;
386
465
  const first = this.rowsOf(item).find((row) => !row.disabled);
387
466
  await first?.updateComplete;
388
- first?.focusItem();
467
+ this.reach(first);
389
468
  });
390
469
  }
391
470
  leave() {
471
+ this.requestFilter('');
392
472
  if (this.flies) {
393
473
  const holder = this.keyPage;
394
474
  if (!holder) {
@@ -396,7 +476,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
396
476
  return;
397
477
  }
398
478
  this.openPage(this.holderOf(holder));
399
- this.updateComplete.then(() => holder.focus());
479
+ this.updateComplete.then(() => this.reach(holder));
400
480
  return;
401
481
  }
402
482
  if (this.drilldown.atRoot) {
@@ -405,7 +485,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
405
485
  }
406
486
  const leaving = this.drilldown.page;
407
487
  this.drilldown.back();
408
- this.updateComplete.then(() => leaving?.focus());
488
+ this.updateComplete.then(() => this.reach(leaving ?? undefined));
489
+ }
490
+ revealRow(item) {
491
+ const holder = this.flies ? this.holderOf(item) : null;
492
+ const surface = holder
493
+ ? holder.shadowRoot?.querySelector('[part="surface"]')
494
+ : this.partNamed('surface');
495
+ scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
409
496
  }
410
497
  handleClosedKeydown(event) {
411
498
  if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
@@ -462,6 +549,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
462
549
  this.addEventListener('focusin', this.handleFocusIn);
463
550
  this.addEventListener('contextmenu', this.handleContextMenu);
464
551
  this.addEventListener('pointerdown', this.handlePointerDown);
552
+ this.addEventListener('mousedown', this.handleMouseDown);
465
553
  this.addEventListener('pointermove', this.handlePointerMove);
466
554
  this.addEventListener('pointerup', this.handlePointerUp);
467
555
  this.addEventListener('pointercancel', this.handlePointerCancel);
@@ -484,6 +572,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
484
572
  this.removeEventListener('focusin', this.handleFocusIn);
485
573
  this.removeEventListener('contextmenu', this.handleContextMenu);
486
574
  this.removeEventListener('pointerdown', this.handlePointerDown);
575
+ this.removeEventListener('mousedown', this.handleMouseDown);
576
+ if (this.filterable) {
577
+ markMatches(this, [], '');
578
+ }
487
579
  this.removeEventListener('pointermove', this.handlePointerMove);
488
580
  this.removeEventListener('pointerup', this.handlePointerUp);
489
581
  this.removeEventListener('pointercancel', this.handlePointerCancel);
@@ -505,7 +597,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
505
597
  const top = this.flies || this.drilldown.atRoot;
506
598
  for (const child of this.children) {
507
599
  if (isSection(child)) {
508
- child.onPage = top;
600
+ child.onPage =
601
+ top &&
602
+ (!this.filtering || [...child.children].some((row) => isItem(row) && this.kept(row)));
509
603
  for (const row of child.children) {
510
604
  if (row.isMenuItem === true) {
511
605
  this.mark(row, top);
@@ -522,9 +616,18 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
522
616
  this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
523
617
  this.markColumns();
524
618
  this.nameSurface();
619
+ if (this.filterable) {
620
+ markMatches(this, this.rowsOf(this.filterPage).filter((row) => this.kept(row)), this.filtering ? this.filter.trim() : '');
621
+ const items = this.items;
622
+ if (changed.has('filter') &&
623
+ this.open &&
624
+ !items.includes(this.highlighted)) {
625
+ this.reach(items[0]);
626
+ }
627
+ }
525
628
  }
526
629
  nameSurface() {
527
- const surface = this.partNamed('surface');
630
+ const surface = this.menuBox;
528
631
  if (!surface || !('ariaLabelledByElements' in surface)) {
529
632
  return;
530
633
  }
@@ -544,7 +647,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
544
647
  }
545
648
  mark(item, onPage) {
546
649
  const page = this.drilldown.page;
547
- item.onPage = onPage;
650
+ item.onPage = onPage && this.kept(item);
651
+ item.virtual = this.filterable;
548
652
  const holds = page !== null && item.contains(page);
549
653
  item.flyout = this.flies;
550
654
  item.expanded = this.flies && holds;
@@ -553,7 +657,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
553
657
  for (const child of item.children) {
554
658
  const nested = child;
555
659
  if (nested.isMenuItem === true && child.slot === 'submenu') {
556
- this.mark(nested, onPage && holds);
660
+ this.mark(nested, item.onPage && holds);
557
661
  }
558
662
  }
559
663
  return;
@@ -568,6 +672,32 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
568
672
  }
569
673
  render() {
570
674
  const point = this.context && this.point;
675
+ const filtering = this.filtering;
676
+ const none = filtering && !this.rowsOf(this.filterPage).some((row) => this.kept(row));
677
+ this.announced = none ? this.noResultsText : '';
678
+ const rows = html `${this.flies || this.drilldown.atRoot
679
+ ? nothing
680
+ : html `<div part="header">
681
+ <button
682
+ part="back"
683
+ type="button"
684
+ role="menuitem"
685
+ tabindex="-1"
686
+ aria-label=${this.backLabel}
687
+ @click=${() => this.leave()}
688
+ >
689
+ <svg part="back-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
690
+ ${unsafeSVG(caretLeft.content)}
691
+ </svg>
692
+ </button>
693
+ <span part="title">${this.drilldown.label}</span>
694
+ </div>`}
695
+ <slot @slotchange=${(event) => this.countRows(event)}></slot>
696
+ ${none
697
+ ? html `<p part="empty"><slot name="no-results">${this.noResultsText}</slot></p>`
698
+ : this.renderStatus(this.hasRows)}`;
699
+ const busy = this.loading ? 'true' : nothing;
700
+ const label = this.label || nothing;
571
701
  return html `<slot name="trigger" @click=${this.handleTriggerClick}></slot>${point
572
702
  ? html `<span
573
703
  id="point"
@@ -578,31 +708,27 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
578
708
  <div
579
709
  part="surface"
580
710
  popover="manual"
581
- role="menu"
582
- aria-busy=${this.loading ? 'true' : nothing}
583
- aria-label=${this.label || nothing}
711
+ role=${this.filterable ? nothing : 'menu'}
712
+ aria-busy=${this.filterable ? nothing : busy}
713
+ aria-label=${this.filterable ? nothing : label}
584
714
  data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
715
+ data-filtered=${filtering ? '' : nothing}
585
716
  ?hidden=${!this.open}
586
717
  >
587
- ${this.flies || this.drilldown.atRoot
588
- ? nothing
589
- : html `<div part="header">
590
- <button
591
- part="back"
592
- type="button"
593
- role="menuitem"
594
- tabindex="-1"
595
- aria-label=${this.backLabel}
596
- @click=${() => this.leave()}
597
- >
598
- <svg part="back-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
599
- ${unsafeSVG(caretLeft.content)}
600
- </svg>
601
- </button>
602
- <span part="title">${this.drilldown.label}</span>
603
- </div>`}
604
- <slot @slotchange=${(event) => this.countRows(event)}></slot>
605
- ${this.renderStatus(this.hasRows)}
718
+ ${this.filterable
719
+ ? html `<input
720
+ part="search-field"
721
+ type="text"
722
+ autocomplete="off"
723
+ spellcheck="false"
724
+ .value=${this.filter}
725
+ placeholder=${this.filterLabel}
726
+ aria-label=${this.filterLabel}
727
+ aria-autocomplete="list"
728
+ aria-controls="items"
729
+ @input=${this.handleFilterInput}
730
+ /><div id="items" role="menu" aria-busy=${busy} aria-label=${label}>${rows}</div>`
731
+ : rows}
606
732
  </div>${this.renderLive(this.hasRows)}`;
607
733
  }
608
734
  };
@@ -616,6 +742,19 @@ __decorate([
616
742
  __decorate([
617
743
  property({ reflect: true, type: Boolean })
618
744
  ], HelixHeadlessMenu.prototype, "context", void 0);
745
+ __decorate([
746
+ property()
747
+ ], HelixHeadlessMenu.prototype, "filter", void 0);
748
+ __decorate([
749
+ property({ type: Boolean })
750
+ ], HelixHeadlessMenu.prototype, "filterable", void 0);
751
+ __decorate([
752
+ words,
753
+ property({ attribute: 'filter-label' })
754
+ ], HelixHeadlessMenu.prototype, "filterLabel", void 0);
755
+ __decorate([
756
+ property({ attribute: false })
757
+ ], HelixHeadlessMenu.prototype, "filterMatch", void 0);
619
758
  __decorate([
620
759
  property()
621
760
  ], HelixHeadlessMenu.prototype, "label", void 0);
@@ -623,6 +762,10 @@ __decorate([
623
762
  words,
624
763
  property({ attribute: 'long-press-label' })
625
764
  ], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
765
+ __decorate([
766
+ words,
767
+ property({ attribute: 'no-results-text' })
768
+ ], HelixHeadlessMenu.prototype, "noResultsText", void 0);
626
769
  __decorate([
627
770
  property({ type: Number })
628
771
  ], HelixHeadlessMenu.prototype, "offset", void 0);
@@ -390,6 +390,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
390
390
  private counted;
391
391
  /** Says how many entries the filter matched, and nothing for an empty one. */
392
392
  private tell;
393
+ /**
394
+ * Moves focus to the row the keys move from, or to the tree while that row
395
+ * is scrolled out of the window, as Tab reaches them.
396
+ */
397
+ focus(options?: FocusOptions): void;
393
398
  protected firstUpdated(changed: PropertyValues): void;
394
399
  protected updated(changed: PropertyValues): void;
395
400
  /**
@@ -268,6 +268,11 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
268
268
  this.status.say(this.counted('result', this.found.size));
269
269
  }
270
270
  }
271
+ focus(options) {
272
+ this.renderRoot
273
+ ?.querySelector('#tree[tabindex="0"], #tree [tabindex="0"]')
274
+ ?.focus(options);
275
+ }
271
276
  firstUpdated(changed) {
272
277
  super.firstUpdated(changed);
273
278
  this.finishHydration();
@@ -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>`;
@@ -158,6 +158,13 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
158
158
  * calls to `next()` in one task then move two stages.
159
159
  */
160
160
  private get at();
161
+ /**
162
+ * Requests a new current stage, and returns whether it moved.
163
+ *
164
+ * Where only a step's `current` says where the reader is, `active` is first
165
+ * written with that step, so `helix-activate` names it as `previous`.
166
+ */
167
+ requestActive(next: string, cause?: string): boolean;
161
168
  /**
162
169
  * Moves to the next stage that can be gone to, as Ark UI's `goToNextStep`
163
170
  * does. Returns whether it moved: not past the last stage, not into a stage
@@ -65,6 +65,13 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
65
65
  const item = this.resolveActive(steps);
66
66
  return item ? steps.indexOf(item) : -1;
67
67
  }
68
+ requestActive(next, cause) {
69
+ const here = this.active === '' ? this.steps[this.at]?.value : undefined;
70
+ if (here) {
71
+ this.active = here;
72
+ }
73
+ return super.requestActive(next, cause);
74
+ }
68
75
  next() {
69
76
  return this.step(1);
70
77
  }
@@ -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. */