@fusion.dev/helix-headless 0.150.0 → 0.152.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 (75) hide show
  1. package/custom-elements.json +1388 -93
  2. package/dist/app-header/root/app-header-root.d.ts +75 -0
  3. package/dist/app-header/root/app-header-root.js +140 -5
  4. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  5. package/dist/area-chart/root/area-chart-root.js +17 -2
  6. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  7. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  8. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +11 -2
  9. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +11 -2
  10. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +14 -2
  11. package/dist/breadcrumbs/root/breadcrumbs-root.js +16 -3
  12. package/dist/callout/root/callout-root.d.ts +20 -0
  13. package/dist/callout/root/callout-root.js +19 -4
  14. package/dist/chat/composer/chat-composer.d.ts +30 -0
  15. package/dist/chat/composer/chat-composer.js +47 -2
  16. package/dist/chat/root/chat-root.d.ts +10 -0
  17. package/dist/chat/root/chat-root.js +14 -0
  18. package/dist/data-table/root/data-table-root.d.ts +34 -2
  19. package/dist/data-table/root/data-table-root.js +63 -2
  20. package/dist/dialog/description.d.ts +10 -0
  21. package/dist/dialog/description.js +22 -8
  22. package/dist/dialog/root/dialog-root.d.ts +17 -1
  23. package/dist/dialog/root/dialog-root.js +2 -2
  24. package/dist/divider/root/divider-root.d.ts +8 -6
  25. package/dist/divider/root/divider-root.js +10 -2
  26. package/dist/drawer/root/drawer-root.d.ts +38 -2
  27. package/dist/drawer/root/drawer-root.js +33 -6
  28. package/dist/element-events.generated.js +4 -0
  29. package/dist/kbd/root/kbd-root.d.ts +7 -1
  30. package/dist/kbd/root/kbd-root.js +8 -4
  31. package/dist/lightbox/root/lightbox-root.d.ts +44 -6
  32. package/dist/lightbox/root/lightbox-root.js +59 -11
  33. package/dist/lightbox/zoom-controller.d.ts +5 -0
  34. package/dist/lightbox/zoom-controller.js +8 -0
  35. package/dist/line-chart/root/line-chart-root.d.ts +36 -0
  36. package/dist/line-chart/root/line-chart-root.js +15 -2
  37. package/dist/menu/root/menu-root.js +2 -0
  38. package/dist/navigation/root/navigation-root.d.ts +11 -1
  39. package/dist/navigation/root/navigation-root.js +10 -5
  40. package/dist/notifications/root/notifications-root.d.ts +17 -0
  41. package/dist/notifications/root/notifications-root.js +21 -1
  42. package/dist/popover/root/popover-root.d.ts +18 -7
  43. package/dist/popover/root/popover-root.js +8 -1
  44. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  45. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  46. package/dist/splitter/root/splitter-root.d.ts +41 -0
  47. package/dist/splitter/root/splitter-root.js +77 -0
  48. package/dist/table/column/table-column.js +3 -2
  49. package/dist/table/grid-controller.d.ts +14 -2
  50. package/dist/table/grid-controller.js +27 -1
  51. package/dist/table/root/table-root.d.ts +30 -4
  52. package/dist/table/root/table-root.js +45 -7
  53. package/dist/table/row/table-row.d.ts +12 -2
  54. package/dist/table/row/table-row.js +8 -2
  55. package/dist/tabs/root/tabs-root.d.ts +25 -0
  56. package/dist/tabs/root/tabs-root.js +28 -1
  57. package/dist/tour/naming.d.ts +9 -3
  58. package/dist/tour/naming.js +9 -3
  59. package/dist/tour/root/tour-root.d.ts +29 -5
  60. package/dist/tour/root/tour-root.js +13 -1
  61. package/dist/tour/stop/tour-stop.d.ts +2 -1
  62. package/dist/tour/stop/tour-stop.js +4 -1
  63. package/dist/tree/root/tree-root.d.ts +13 -0
  64. package/dist/tree/root/tree-root.js +42 -2
  65. package/dist/video/root/video-root.d.ts +6 -0
  66. package/dist/video/root/video-root.js +5 -1
  67. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  68. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  69. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  70. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  71. package/lint.json +1 -1
  72. package/package.json +5 -5
  73. package/types/jsx.d.ts +1098 -128
  74. package/types/vue.d.ts +248 -59
  75. package/types/words.d.ts +12 -2
@@ -11,6 +11,7 @@ import { property, state } from 'lit/decorators.js';
11
11
  import { snapTo } from '../snap.js';
12
12
  const STACK_BELOW = 512;
13
13
  const channel = new WeakMap();
14
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
14
15
  let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
15
16
  static { HelixHeadlessSplitter_1 = this; }
16
17
  static { this.words = 'helix-splitter'; }
@@ -69,6 +70,41 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
69
70
  this.reached = undefined;
70
71
  this.requestUpdate('position', previous);
71
72
  }
73
+ watchLength() {
74
+ if (!this.primary || typeof ResizeObserver === 'undefined') {
75
+ this.lengths?.disconnect();
76
+ this.lengths = undefined;
77
+ this.length = 0;
78
+ return;
79
+ }
80
+ if (this.lengths) {
81
+ return;
82
+ }
83
+ this.lengths = new ResizeObserver(([entry]) => {
84
+ const box = entry?.borderBoxSize?.[0];
85
+ const length = (this.vertical
86
+ ? (box?.blockSize ?? entry?.contentRect.height)
87
+ : (box?.inlineSize ?? entry?.contentRect.width)) ?? 0;
88
+ this.holdPrimary(length);
89
+ });
90
+ this.lengths.observe(this);
91
+ }
92
+ holdPrimary(length) {
93
+ const before = this.length;
94
+ this.length = length;
95
+ if (!this.primary || before <= 0 || length <= 0 || length === before) {
96
+ return;
97
+ }
98
+ if (this.collapsed || this.dragging) {
99
+ return;
100
+ }
101
+ const position = this.position;
102
+ const kept = ((this.primary === 'start' ? position : 100 - position) / 100) * before;
103
+ const share = (kept / length) * 100;
104
+ const previous = position;
105
+ this.positionState = this.clamp(this.primary === 'start' ? share : 100 - share);
106
+ this.requestUpdate('position', previous);
107
+ }
72
108
  get showing() {
73
109
  return this.reached === undefined ? this.position : this.clamp(this.reached);
74
110
  }
@@ -225,6 +261,34 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
225
261
  });
226
262
  this.max = 100;
227
263
  this.min = 0;
264
+ this.length = 0;
265
+ this.handlePaneKeys = (event) => {
266
+ if (event.key !== 'F6' ||
267
+ event.altKey ||
268
+ event.ctrlKey ||
269
+ event.metaKey ||
270
+ event.defaultPrevented) {
271
+ return;
272
+ }
273
+ const path = event.composedPath();
274
+ const pane = (name) => this.renderRoot
275
+ ?.querySelector(`slot[name="${name}"]`)
276
+ ?.assignedElements({ flatten: true }) ?? [];
277
+ const inner = path.find((node) => node instanceof HelixHeadlessSplitter_1);
278
+ if (inner !== this) {
279
+ return;
280
+ }
281
+ const fromStart = pane('start').some((one) => path.includes(one));
282
+ const to = pane(fromStart ? 'end' : 'start');
283
+ const first = to.flatMap((one) => [
284
+ ...(one instanceof HTMLElement && one.matches(FOCUSABLE) ? [one] : []),
285
+ ...one.querySelectorAll(FOCUSABLE),
286
+ ])[0];
287
+ if (first) {
288
+ event.preventDefault();
289
+ first.focus();
290
+ }
291
+ };
228
292
  this.snap = '';
229
293
  this.snapThreshold = 12;
230
294
  this.step = 2;
@@ -435,13 +499,19 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
435
499
  super.connectedCallback();
436
500
  if (this.hasUpdated) {
437
501
  this.watchWidth();
502
+ this.watchLength();
438
503
  this.findOuter();
439
504
  }
505
+ this.addEventListener('keydown', this.handlePaneKeys);
440
506
  }
441
507
  disconnectedCallback() {
442
508
  super.disconnectedCallback();
509
+ this.removeEventListener('keydown', this.handlePaneKeys);
443
510
  this.sizes?.disconnect();
444
511
  this.sizes = undefined;
512
+ this.lengths?.disconnect();
513
+ this.lengths = undefined;
514
+ this.length = 0;
445
515
  if (this.outer) {
446
516
  channel.get(this.outer)?.uncross(this);
447
517
  }
@@ -451,6 +521,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
451
521
  firstUpdated(changed) {
452
522
  super.firstUpdated(changed);
453
523
  this.watchWidth();
524
+ this.watchLength();
454
525
  this.findOuter();
455
526
  this.finishHydration();
456
527
  }
@@ -459,6 +530,9 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
459
530
  if (changed.has('orientation')) {
460
531
  this.watchWidth();
461
532
  }
533
+ if (changed.has('primary')) {
534
+ this.watchLength();
535
+ }
462
536
  if (this.collapsed) {
463
537
  this.dataset.collapsed = this.collapsed;
464
538
  }
@@ -578,6 +652,9 @@ __decorate([
578
652
  __decorate([
579
653
  property({ type: Number })
580
654
  ], HelixHeadlessSplitter.prototype, "position", null);
655
+ __decorate([
656
+ property()
657
+ ], HelixHeadlessSplitter.prototype, "primary", void 0);
581
658
  __decorate([
582
659
  property()
583
660
  ], HelixHeadlessSplitter.prototype, "snap", void 0);
@@ -174,6 +174,7 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
174
174
  const named = { name, named: name ? 'yes' : 'no' };
175
175
  const grid = this.gridCell;
176
176
  const control = grid ? '-1' : nothing;
177
+ const width = Math.min(4096, Math.max(HelixHeadlessTableColumn_1.NARROWEST, this.drawn));
177
178
  return html `<span
178
179
  part="column"
179
180
  role=${grid ? 'columnheader' : nothing}
@@ -197,8 +198,8 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
197
198
  aria-orientation="vertical"
198
199
  aria-valuemin=${HelixHeadlessTableColumn_1.NARROWEST}
199
200
  aria-valuemax="4096"
200
- aria-valuenow=${Math.max(HelixHeadlessTableColumn_1.NARROWEST, this.drawn)}
201
- aria-valuetext=${formatWords(this, 'widthLabel', { width: this.drawn })}
201
+ aria-valuenow=${width}
202
+ aria-valuetext=${formatWords(this, 'widthLabel', { width })}
202
203
  @keydown=${this.handleResizeKey}
203
204
  @pointerdown=${this.handleResizeStart}
204
205
  @pointermove=${this.handleResizeMove}
@@ -24,6 +24,16 @@ export interface TableGridOptions {
24
24
  part: (name: string) => HTMLElement | null;
25
25
  /** The rows in the order they are drawn. */
26
26
  rows: () => Element[];
27
+ /**
28
+ * The words each row is found by when a reader types, in the order the rows
29
+ * are drawn.
30
+ */
31
+ rowWords: () => string[];
32
+ /**
33
+ * Selects every row from the row last chosen, or from row `from` where none
34
+ * was, to row `to`, and says whether the table selects at all.
35
+ */
36
+ selectRange: (to: number, from: number) => boolean;
27
37
  /** Selects every row for Control or Command with A, and says whether it did. */
28
38
  selectAll: () => boolean;
29
39
  /** Whether the table is a grid now. */
@@ -31,8 +41,8 @@ export interface TableGridOptions {
31
41
  }
32
42
  /**
33
43
  * The keyboard of a `grid` table: one tab stop among its cells, the arrow
34
- * keys, Home, End and the Page keys between them, and Enter or F2 into a
35
- * cell's controls.
44
+ * keys, Home, End and the Page keys between them, Enter or F2 into a cell's
45
+ * controls, typeahead over the rows, and Shift ranges of rows.
36
46
  *
37
47
  * **Tab leaves the grid.** The controls in a cell are the consumer's, so their
38
48
  * tab order is left as written: Tab on a cell, or past the last control of a
@@ -59,6 +69,8 @@ export declare class TableGridController implements ReactiveController {
59
69
  private pointing;
60
70
  /** The cells given a `tabindex`, so a table that stops being a grid takes it back. */
61
71
  private readonly given;
72
+ /** The letters a reader has typed at the rows. */
73
+ private readonly typeahead;
62
74
  constructor(host: ReactiveControllerHost & HTMLElement, options: TableGridOptions);
63
75
  hostConnected(): void;
64
76
  hostDisconnected(): void;
@@ -1,4 +1,4 @@
1
- import { deepActiveElement, deepContains, focusableElements, gridMove, inlineStep, pageRows, scrollingAncestor, scrollRoom, selectsAll, whenHydrated, } from '@fusion.dev/helix-core';
1
+ import { deepActiveElement, deepContains, focusableElements, gridMove, inlineStep, pageRows, scrollingAncestor, scrollRoom, selectsAll, Typeahead, typesCharacter, whenHydrated, } from '@fusion.dev/helix-core';
2
2
  const HEADS = { grip: 'grip-head', select: 'select-all' };
3
3
  const shown = (element) => {
4
4
  const hidden = element.getAttribute('hidden');
@@ -13,6 +13,7 @@ export class TableGridController {
13
13
  this.focusWanted = false;
14
14
  this.pointing = false;
15
15
  this.given = new Set();
16
+ this.typeahead = new Typeahead();
16
17
  this.handleKeydown = (event) => {
17
18
  if (!this.options.on() || event.defaultPrevented || event.altKey) {
18
19
  return;
@@ -35,6 +36,25 @@ export class TableGridController {
35
36
  }
36
37
  return;
37
38
  }
39
+ if (typesCharacter(event) && (event.key !== ' ' || this.typeahead.typing())) {
40
+ const found = this.typeahead.match(event.key, this.options.rowWords(), place.row);
41
+ if (found >= 0) {
42
+ event.preventDefault();
43
+ if (found !== place.row) {
44
+ this.moveTo({
45
+ column: Math.max(0, Math.min(place.column, this.widthOf(found) - 1)),
46
+ row: found,
47
+ });
48
+ }
49
+ }
50
+ return;
51
+ }
52
+ if (event.key === ' ' && event.shiftKey && place.row >= 0) {
53
+ if (this.options.selectRange(place.row, place.row)) {
54
+ event.preventDefault();
55
+ }
56
+ return;
57
+ }
38
58
  if (event.key === 'Enter' || event.key === ' ' || event.key === 'F2') {
39
59
  if (event.key !== 'F2' && this.options.activate(place, event)) {
40
60
  event.preventDefault();
@@ -57,6 +77,12 @@ export class TableGridController {
57
77
  }
58
78
  event.preventDefault();
59
79
  to.column = Math.max(0, Math.min(to.column, this.widthOf(to.row) - 1));
80
+ if (event.shiftKey &&
81
+ (event.key === 'ArrowUp' || event.key === 'ArrowDown') &&
82
+ place.row >= 0 &&
83
+ to.row >= 0) {
84
+ this.options.selectRange(to.row, place.row);
85
+ }
60
86
  if (to.row !== place.row || to.column !== place.column) {
61
87
  this.moveTo(to);
62
88
  }
@@ -76,7 +76,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
76
76
  * through cell by cell, the headings included, as React Aria's table is. The
77
77
  * table is then a `grid` and each value a `gridcell`, and the table gives
78
78
  * every cell a `tabindex` so focus can rest on it: set `grid` only where the
79
- * cells may carry one. For typeahead and range selection, use `DataTable`.
79
+ * cells may carry one.
80
80
  *
81
81
  * | Key | What it does |
82
82
  * |---|---|
@@ -90,7 +90,10 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
90
90
  * | Enter, Space on a sortable heading | Sorts by the column. |
91
91
  * | Space on a row, Enter or Space on its checkbox cell | Selects the row, in a `selectable` table. |
92
92
  * | Enter or Space on a grip cell | Picks the row up, or drops it, in a `reorderable` table. Up, Down, Home, and End move a row picked up, and Escape puts it back. |
93
+ * | Shift with Down or Up | Selects the rows it moves across, in a `selectable` table. |
94
+ * | Shift with Space | Selects every row from the one last chosen, in a `selectable` table. |
93
95
  * | Control or Command with A | Selects every row, in a `selectable` table. |
96
+ * | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
94
97
  * | Tab | Leaves the table. |
95
98
  *
96
99
  * The CSS custom properties listed for this component are written by it and
@@ -105,7 +108,9 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
105
108
  * @documentation https://fusion.dev/helix/components/table
106
109
  *
107
110
  * @slot - The table's rows.
111
+ * @slot empty - Content shown when there are no rows and none are loading, such as a picture and a link. Falls back to `empty-text`, which the live region still says.
108
112
  * @slot heading - Words that name the table, shown above it, such as a heading.
113
+ * @slot loading - Content shown while the rows load, such as a spinner. Falls back to `loading-text`, which the live region still says.
109
114
  *
110
115
  * @event {HelixBeforeReorderEvent} helix-before-reorder - Fired when a row is dropped at a new place, in a `reorderable` table, with every row's key in the new order. Cancelable: cancel it to keep the row where it is.
111
116
  * @event {HelixReorderEvent} helix-reorder - Fired after `helix-before-reorder` when nobody canceled it. Move your rows to the order in `keys`. Cancelable: cancel it to say the move did not happen.
@@ -144,6 +149,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
144
149
  * `registerWords('de', { 'helix-table': { selectAllLabel: 'Alle Zeilen auswählen' } })`.
145
150
  */
146
151
  static words: string;
152
+ /** The empty and loading lines are the `empty` and `loading` slots. */
153
+ protected statusSlots: boolean;
147
154
  /**
148
155
  * The word for an ascending sort, in a `sorted-label` that writes `{order}`.
149
156
  *
@@ -383,9 +390,25 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
383
390
  private readonly handleToggle;
384
391
  /** Every row's key, in the order the rows are written. */
385
392
  private get everyKey();
386
- /** The checkbox in the heading: every row, or none where every row is chosen. */
393
+ /** The keys of the rows that can be chosen: every row that is not `disabled`. */
394
+ private get enabledKeys();
395
+ /** Whether the row of that key is `disabled`. */
396
+ private isDisabled;
397
+ /**
398
+ * The checkbox in the heading: every row that can be chosen, or none of
399
+ * them where each already is. A `disabled` row keeps what it had.
400
+ */
387
401
  private chooseAll;
388
- /** One row's checkbox, ticked or not. */
402
+ /** The key of the row a Shift range runs from: the one last chosen. */
403
+ private anchorKey;
404
+ /**
405
+ * Selects every row from the one last chosen to the drawn row `to`, keeping
406
+ * what was already selected. With no row to run from, the range is the row
407
+ * the reader is on, `from`, and it becomes the one to run from. Says whether
408
+ * the table selects at all.
409
+ */
410
+ private selectRange;
411
+ /** One row's checkbox, ticked or not. It becomes the row a range runs from. */
389
412
  private chooseRow;
390
413
  /**
391
414
  * Enter, Space or F2 on a cell of a `grid` table, and whether it did
@@ -498,7 +521,10 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
498
521
  * assignment has not happened.
499
522
  */
500
523
  static shadowRootOptions: ShadowRootInit;
501
- /** The rows: every child that is not a column, which is what the body slot is assigned. */
524
+ /**
525
+ * The rows: every child that is not a column, a heading, or the content of
526
+ * the `empty` or `loading` slot, which is what the body slot is assigned.
527
+ */
502
528
  protected get slotted(): Element[];
503
529
  private assign;
504
530
  /**
@@ -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, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, sort, splitValues, toggleEvery, 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';
@@ -44,9 +44,12 @@ const SAMPLE = 200;
44
44
  const textOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim();
45
45
  const lengthOf = (element) => textOf(element).length;
46
46
  const numberOf = (value) => Number(value.replaceAll(',', ''));
47
+ const STATUS = new Set(['empty', 'loading']);
48
+ const NAMED = new Set([...STATUS, 'heading']);
47
49
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
48
50
  constructor() {
49
51
  super(...arguments);
52
+ this.statusSlots = true;
50
53
  this.ascendingLabel = 'ascending';
51
54
  this.canceledLabel = 'Canceled';
52
55
  this.columns = '';
@@ -82,7 +85,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
82
85
  on: () => this.grid,
83
86
  part: (name) => this.partNamed(name),
84
87
  rows: () => (this.childrenKnown ? this.arrangedRows : []),
85
- selectAll: () => this.selectable && this.requestSelected(new Set(this.everyKey)),
88
+ rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
89
+ selectAll: () => this.selectable && this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
90
+ selectRange: (to, from) => this.selectRange(to, from),
86
91
  });
87
92
  this.reorder = new ReorderController(this, {
88
93
  axis: 'block',
@@ -125,6 +130,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
125
130
  event.preventDefault();
126
131
  this.chooseRow(found.key, event.detail.checked);
127
132
  };
133
+ this.anchorKey = undefined;
128
134
  this.handleHeadClick = (event) => {
129
135
  const path = event.composedPath();
130
136
  if (!path.some((node) => node instanceof Element && node.getAttribute('part') === 'sort')) {
@@ -260,10 +266,35 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
260
266
  get everyKey() {
261
267
  return this.keyed.map(({ key }) => key);
262
268
  }
269
+ get enabledKeys() {
270
+ return this.keyed.filter(({ row }) => !row.disabled).map(({ key }) => key);
271
+ }
272
+ isDisabled(key) {
273
+ return this.keyed.find((one) => one.key === key)?.row.disabled === true;
274
+ }
263
275
  chooseAll() {
264
- this.requestSelected(new Set(toggleEvery(this.chosen, this.everyKey)));
276
+ this.requestSelected(new Set(toggleEvery(this.chosen, this.enabledKeys)));
277
+ }
278
+ selectRange(to, from) {
279
+ if (!this.selectable) {
280
+ return false;
281
+ }
282
+ const keyOf = new Map(this.keyed.map(({ key, row }) => [row, key]));
283
+ const keys = this.arrangedRows.map((row) => keyOf.get(row) ?? '');
284
+ const { anchor, anchorKey } = anchorAt(keys, this.anchorKey, from);
285
+ this.anchorKey = anchorKey;
286
+ const now = this.chosen;
287
+ const next = rangeAdd([...now], keys, anchor, to)?.filter((key) => now.has(key) || !this.isDisabled(key));
288
+ if (next && next.length > now.size) {
289
+ this.requestSelected(new Set(next));
290
+ }
291
+ return true;
265
292
  }
266
293
  chooseRow(key, on) {
294
+ if (this.isDisabled(key)) {
295
+ return;
296
+ }
297
+ this.anchorKey = key;
267
298
  const keys = this.chosen;
268
299
  if (on) {
269
300
  keys.add(key);
@@ -298,7 +329,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
298
329
  this.chooseRow(found.key, !this.chosen.has(found.key));
299
330
  return true;
300
331
  }
301
- if (event.key === 'Enter' && row?.href) {
332
+ if (event.key === 'Enter' && row?.href && !row.disabled) {
302
333
  followLink(this.ownerDocument, { href: row.href, rel: row.rel, target: row.target }, event);
303
334
  return true;
304
335
  }
@@ -418,7 +449,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
418
449
  }; }
419
450
  get slotted() {
420
451
  const columns = new Set(this.tableColumns);
421
- return [...(this.children ?? [])].filter((child) => !columns.has(child) && child.getAttribute('slot') !== 'heading');
452
+ return [...(this.children ?? [])].filter((child) => !columns.has(child) && !NAMED.has(child.getAttribute('slot') ?? ''));
422
453
  }
423
454
  assign() {
424
455
  if (this.assignsByName) {
@@ -429,7 +460,14 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
429
460
  }
430
461
  const columns = new Set(this.tableColumns);
431
462
  const headings = new Set(this.headings);
432
- assignSlots(this.renderRoot, [...columns, ...headings, ...this.arrangedRows], (child) => (columns.has(child) ? 'columns' : headings.has(child) ? 'heading' : ''));
463
+ const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
464
+ assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...this.arrangedRows], (child) => columns.has(child)
465
+ ? 'columns'
466
+ : headings.has(child)
467
+ ? 'heading'
468
+ : status.includes(child)
469
+ ? (child.getAttribute('slot') ?? '')
470
+ : '');
433
471
  }
434
472
  alignCells() {
435
473
  if (typeof CSSStyleSheet === 'undefined' || !('replaceSync' in CSSStyleSheet.prototype)) {
@@ -483,7 +521,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
483
521
  if (!this.selectable) {
484
522
  return nothing;
485
523
  }
486
- const every = this.childrenKnown ? this.keyed.map(({ key }) => key) : [];
524
+ const every = this.childrenKnown ? this.enabledKeys : [];
487
525
  const { checked, indeterminate } = everyState(this.chosen, every);
488
526
  const checkbox = this.tag('checkbox');
489
527
  const grid = this.grid;
@@ -13,7 +13,8 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
13
13
  * it, or make it interactive.
14
14
  *
15
15
  * In a `selectable` table the row draws a checkbox cell before its own cells,
16
- * and `value` is the key the table's `selected` lists it by.
16
+ * and `value` is the key the table's `selected` lists it by. `disabled` keeps
17
+ * the row out of the selection and out of its link.
17
18
  *
18
19
  * In a `reorderable` table the row draws a grip cell before those, holding a
19
20
  * button named by the table's `move-row-label`, and `value` is the key
@@ -56,7 +57,7 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
56
57
  * @cssstate dragging - The row is being moved, in a `reorderable` table.
57
58
  * @cssstate drop-after - The row being moved goes after this one when it is dropped.
58
59
  * @cssstate drop-before - The row being moved goes before this one when it is dropped.
59
- * @cssstate link - While the row has `href`, so a press on it follows the link.
60
+ * @cssstate link - While the row has `href` and is not `disabled`, so a press on it follows the link.
60
61
  * @cssstate selected - While the row is selected, in a `selectable` table.
61
62
  */
62
63
  export declare class HelixHeadlessTableRow extends HelixElement {
@@ -65,6 +66,15 @@ export declare class HelixHeadlessTableRow extends HelixElement {
65
66
  * finds its columns. Not a consumer's to read or set.
66
67
  */
67
68
  readonly isTableRow = true;
69
+ /**
70
+ * Takes the row out of the table's selection and out of its link: the row's
71
+ * checkbox is disabled, Space, Shift ranges, and the checkbox in the heading
72
+ * pass over it, and a press does not follow `href`. A row already selected
73
+ * stays selected. A screen reader hears it as unavailable, and in a `grid`
74
+ * table the arrow keys still reach it, as a disabled cell in a grid is
75
+ * reached.
76
+ */
77
+ disabled: boolean;
68
78
  /**
69
79
  * The URL a press on the row follows, which makes the whole row a link.
70
80
  * Empty, the default, leaves presses to the row's cells.
@@ -15,6 +15,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
15
15
  constructor() {
16
16
  super(...arguments);
17
17
  this.isTableRow = true;
18
+ this.disabled = false;
18
19
  this.href = '';
19
20
  this.rel = '';
20
21
  this.target = '';
@@ -30,7 +31,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
30
31
  this.selectStop = false;
31
32
  this.columnRules = undefined;
32
33
  this.handleClick = (event) => {
33
- if (this.href === '' || event.button !== 0 || event.defaultPrevented) {
34
+ if (this.disabled || this.href === '' || event.button !== 0 || event.defaultPrevented) {
34
35
  return;
35
36
  }
36
37
  const cell = event
@@ -68,8 +69,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
68
69
  super.willUpdate(changed);
69
70
  hostAria(this.internals, { role: 'row' });
70
71
  this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
72
+ this.internals.ariaDisabled = this.disabled ? 'true' : null;
71
73
  this.toggleState('selected', this.selectable && this.selected);
72
- this.toggleState('link', this.href !== '');
74
+ this.toggleState('link', this.href !== '' && !this.disabled);
73
75
  this.toggleState('dragging', this.move === 'dragging');
74
76
  this.toggleState('drop-after', this.move === 'drop-after');
75
77
  this.toggleState('drop-before', this.move === 'drop-before');
@@ -100,6 +102,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
100
102
  mark-checked: checkbox-mark-checked, mark-dash: checkbox-mark-dash,
101
103
  mark-indeterminate: checkbox-mark-indeterminate"
102
104
  aria-label=${this.selectLabel}
105
+ ?disabled=${this.disabled}
103
106
  .checked=${this.selected}
104
107
  ></${checkbox}
105
108
  ></span>`;
@@ -126,6 +129,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
126
129
  return html `${this.renderGrip()}${this.renderSelect()}<slot @slotchange=${this.handleSlotChange}></slot>`;
127
130
  }
128
131
  };
132
+ __decorate([
133
+ property({ reflect: true, type: Boolean })
134
+ ], HelixHeadlessTableRow.prototype, "disabled", void 0);
129
135
  __decorate([
130
136
  property()
131
137
  ], HelixHeadlessTableRow.prototype, "href", void 0);
@@ -36,6 +36,20 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
36
36
  * Disabled tabs are passed over. Tab leaves the list for the selected panel,
37
37
  * which is a tab stop only while it holds nothing focusable.
38
38
  *
39
+ * **Keyboard.**
40
+ *
41
+ * | Key | What it does |
42
+ * |---|---|
43
+ * | Right, Down | Moves to the next tab, from the last round to the first. Selects it, unless `manual` is set or a tab is a link. |
44
+ * | Left, Up | Moves to the tab before, from the first round to the last. |
45
+ * | Home, End | Moves to the first or the last tab. |
46
+ * | Enter, Space | Selects the tab with focus, or follows its link. |
47
+ * | Tab | Leaves the list for the selected panel. |
48
+ *
49
+ * Removing the selected tab selects the first enabled tab that is left, and
50
+ * `active` says so, with `helix-activate`. Where the removed tab had focus,
51
+ * focus moves to the tab now selected.
52
+ *
39
53
  * The `indicator` part moves to the selected tab and carries its place and
40
54
  * size, so a mark of your own can be drawn there: style the part, or slot an
41
55
  * element into `indicator`, which sits inside the part and reads the same
@@ -125,8 +139,19 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
125
139
  private get linked();
126
140
  private get panels();
127
141
  private readonly handleSelection;
142
+ /** The values of the tabs the last pairing read, to tell a removed tab from one not yet here. */
143
+ private held;
128
144
  /** Pushes the current value down to whichever tab and panel own it. */
129
145
  private sync;
146
+ /** The tab that last had focus, so its removal can hand focus on. */
147
+ private focused;
148
+ private readonly handleFocusIn;
149
+ /**
150
+ * Puts focus on the selected tab where the tab that had it was removed and
151
+ * focus fell to the page, which is where removing a focused element leaves
152
+ * it.
153
+ */
154
+ private refocus;
130
155
  connectedCallback(): void;
131
156
  disconnectedCallback(): void;
132
157
  protected firstUpdated(changed: PropertyValues): void;
@@ -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, customElement, HelixElement, IndicatorController, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, deepActiveElement, HelixActivateEvent, HelixElement, IndicatorController, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessTabPanel } from '../panel/tab-panel.js';
@@ -35,9 +35,15 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
35
35
  }
36
36
  this.requestActive(event.detail.value);
37
37
  };
38
+ this.held = [];
39
+ this.handleFocusIn = (event) => {
40
+ const path = event.composedPath();
41
+ this.focused = this.tabs.find((tab) => path.includes(tab));
42
+ };
38
43
  this.handleSlotChange = () => {
39
44
  this.sync();
40
45
  this.indicator.place();
46
+ this.refocus();
41
47
  };
42
48
  }
43
49
  static { this.words = 'helix-tabs'; }
@@ -54,6 +60,13 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
54
60
  const tabs = this.tabs;
55
61
  const declared = this.active === '' ? tabs.find((tab) => tab.selected) : undefined;
56
62
  const value = (declared ?? this.resolveActive(tabs))?.value ?? '';
63
+ const previous = this.active;
64
+ const gone = this.held.includes(previous) && !tabs.some((tab) => tab.value === previous);
65
+ this.held = tabs.map((tab) => tab.value);
66
+ if (gone && value !== '') {
67
+ this.active = value;
68
+ this.dispatchEvent(new HelixActivateEvent({ previous, value }));
69
+ }
57
70
  for (const tab of tabs) {
58
71
  const selected = tab.value === value;
59
72
  whenHydrated(tab, () => {
@@ -77,13 +90,27 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
77
90
  });
78
91
  }
79
92
  }
93
+ refocus() {
94
+ const gone = this.focused;
95
+ if (!gone || this.tabs.includes(gone)) {
96
+ return;
97
+ }
98
+ this.focused = undefined;
99
+ const active = deepActiveElement();
100
+ if (active !== null && active !== document.body) {
101
+ return;
102
+ }
103
+ void this.updateComplete.then(() => this.tabs.find((tab) => tab.selected)?.focus());
104
+ }
80
105
  connectedCallback() {
81
106
  super.connectedCallback();
82
107
  this.addEventListener('helix-select', this.handleSelection);
108
+ this.addEventListener('focusin', this.handleFocusIn);
83
109
  }
84
110
  disconnectedCallback() {
85
111
  super.disconnectedCallback();
86
112
  this.removeEventListener('helix-select', this.handleSelection);
113
+ this.removeEventListener('focusin', this.handleFocusIn);
87
114
  }
88
115
  firstUpdated(changed) {
89
116
  super.firstUpdated(changed);
@@ -15,6 +15,11 @@ export interface StopWords {
15
15
  heading: readonly Element[];
16
16
  /** Whether the stop has a `label` of its own. */
17
17
  labeled: boolean;
18
+ /**
19
+ * The region that holds the content, given where the content holds words
20
+ * with no element around them, which then describes the panel.
21
+ */
22
+ loose?: Element | null;
18
23
  /** The element that says the stop's place in the tour, such as "2 of 5". */
19
24
  progress: Element | null;
20
25
  }
@@ -24,7 +29,8 @@ export interface StopWords {
24
29
  * Named by the `heading` slot, then by the first heading in the content, then
25
30
  * by the stop's own `label`, and where there is none of those, by its place in
26
31
  * the tour, so no stop is an unnamed group. Described by the `description`
27
- * slot and by its place, which is how a screen reader hears where it is as
28
- * focus moves from one stop to the next.
32
+ * slot, or where it is empty by the words at the start of the content, as a
33
+ * `Dialog` is, and then by its place, which is how a screen reader hears where
34
+ * it is as focus moves from one stop to the next.
29
35
  */
30
- export declare const stopNaming: ({ content, description, heading, labeled, progress, }: StopWords) => StopNaming;
36
+ export declare const stopNaming: ({ content, description, heading, labeled, loose, progress, }: StopWords) => StopNaming;