@fusion.dev/helix-headless 0.155.0 → 0.156.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 (54) hide show
  1. package/custom-elements.json +1787 -92
  2. package/dist/canvas/node/canvas-node.js +1 -1
  3. package/dist/canvas/root/canvas-root.d.ts +17 -2
  4. package/dist/canvas/root/canvas-root.js +23 -6
  5. package/dist/chat/composer/chat-composer.js +1 -1
  6. package/dist/chat/root/chat-root.d.ts +14 -1
  7. package/dist/chat/root/chat-root.js +14 -4
  8. package/dist/combobox/root/combobox-root.d.ts +10 -0
  9. package/dist/combobox/root/combobox-root.js +18 -3
  10. package/dist/command/root/command-root.d.ts +116 -1
  11. package/dist/command/root/command-root.js +124 -15
  12. package/dist/data-table/root/data-table-root.d.ts +25 -0
  13. package/dist/data-table/root/data-table-root.js +13 -3
  14. package/dist/date-input/root/date-input-root.d.ts +32 -1
  15. package/dist/date-input/root/date-input-root.js +7 -1
  16. package/dist/element-events.generated.js +1 -0
  17. package/dist/graph/root/graph-root.d.ts +13 -0
  18. package/dist/graph/root/graph-root.js +16 -0
  19. package/dist/list/root/list-root.d.ts +74 -1
  20. package/dist/list/root/list-root.js +92 -4
  21. package/dist/list/selection.js +3 -3
  22. package/dist/map/root/map-root.d.ts +6 -0
  23. package/dist/map/root/map-root.js +8 -1
  24. package/dist/number-input/root/number-input-root.d.ts +63 -6
  25. package/dist/number-input/root/number-input-root.js +96 -11
  26. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  27. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  28. package/dist/option/root/option-root.d.ts +32 -0
  29. package/dist/option/root/option-root.js +44 -5
  30. package/dist/outline/root/outline-root.d.ts +6 -2
  31. package/dist/outline/root/outline-root.js +7 -3
  32. package/dist/table/root/table-root.d.ts +20 -0
  33. package/dist/table/root/table-root.js +13 -3
  34. package/dist/time-input/root/time-input-root.d.ts +32 -1
  35. package/dist/time-input/root/time-input-root.js +9 -2
  36. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  37. package/dist/time-picker/root/time-picker-root.js +58 -7
  38. package/dist/timeline/item/timeline-item.d.ts +25 -0
  39. package/dist/timeline/item/timeline-item.js +27 -2
  40. package/dist/video/root/keys.d.ts +6 -2
  41. package/dist/video/root/keys.js +18 -1
  42. package/dist/video/root/video-root.d.ts +60 -1
  43. package/dist/video/root/video-root.js +70 -2
  44. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  45. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  46. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  47. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  48. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  49. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  50. package/lint.json +1 -1
  51. package/package.json +5 -5
  52. package/types/jsx.d.ts +2901 -232
  53. package/types/vue.d.ts +824 -58
  54. package/types/words.d.ts +110 -0
@@ -12,6 +12,9 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { decimals, formatGrain, strideOf } from '../grain.js';
13
13
  const SCRUB_PITCH = 4;
14
14
  const SCRUB_FINE = 16;
15
+ const PLAIN = /^-?(\d+(\.\d*)?|\.\d+)$/;
16
+ const REPEAT_DELAY = 400;
17
+ const REPEAT_EVERY = 60;
15
18
  let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinControllerValue(MixinField(HelixFormElement)) {
16
19
  constructor() {
17
20
  super(...arguments);
@@ -28,6 +31,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
28
31
  this.rangeOverflowMessage = 'Value must be {max} or less.';
29
32
  this.rangeUnderflowMessage = 'Value must be {min} or more.';
30
33
  this.readOnly = false;
34
+ this.roleLabel = 'number field';
31
35
  this.scrub = false;
32
36
  this.stepMismatchMessage = 'Please enter a valid value. The two nearest valid values are {below} and {above}.';
33
37
  this.valueText = '';
@@ -49,6 +53,53 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
49
53
  this.editing = true;
50
54
  };
51
55
  this.touchApple = appleTouch(typeof navigator === 'undefined' ? undefined : navigator);
56
+ this.pressStepped = false;
57
+ this.handleStepperDown = (event) => {
58
+ if (event.button !== 0) {
59
+ return;
60
+ }
61
+ const button = event.currentTarget;
62
+ const up = button.getAttribute('part') === 'increment';
63
+ clearTimeout(this.repeat);
64
+ this.pressStepped = true;
65
+ button.setPointerCapture?.(event.pointerId);
66
+ const step = () => {
67
+ const before = this.value;
68
+ if (up) {
69
+ this.stepUp();
70
+ }
71
+ else {
72
+ this.stepDown();
73
+ }
74
+ this.repeat = this.value === before ? undefined : setTimeout(step, REPEAT_EVERY);
75
+ };
76
+ step();
77
+ if (this.repeat !== undefined) {
78
+ clearTimeout(this.repeat);
79
+ this.repeat = setTimeout(step, REPEAT_DELAY);
80
+ }
81
+ };
82
+ this.stopRepeat = () => {
83
+ clearTimeout(this.repeat);
84
+ this.repeat = undefined;
85
+ if (this.pressStepped) {
86
+ setTimeout(() => {
87
+ this.pressStepped = false;
88
+ });
89
+ }
90
+ };
91
+ this.handleStepperClick = (event) => {
92
+ if (this.pressStepped) {
93
+ this.pressStepped = false;
94
+ return;
95
+ }
96
+ if (event.currentTarget.getAttribute('part') === 'increment') {
97
+ this.stepUp();
98
+ }
99
+ else {
100
+ this.stepDown();
101
+ }
102
+ };
52
103
  this.scrubFrom = 0;
53
104
  this.scrubValue = 0;
54
105
  this.scrubStart = '';
@@ -240,6 +291,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
240
291
  stepDown(large = false) {
241
292
  this.nudge(large ? -(this.largeStep ?? 10) : -1);
242
293
  }
294
+ disconnectedCallback() {
295
+ super.disconnectedCallback();
296
+ this.stopRepeat();
297
+ }
243
298
  renderLabel() {
244
299
  if (!this.scrub || !this.labeled) {
245
300
  return super.renderLabel();
@@ -267,13 +322,20 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
267
322
  restoreValue(control, this.shown);
268
323
  return this.value !== before;
269
324
  }
325
+ submitted(parsed) {
326
+ const text = this.value.trim();
327
+ if (parsed === null) {
328
+ return '';
329
+ }
330
+ return PLAIN.test(text) ? text : String(parsed);
331
+ }
270
332
  sync() {
271
333
  const control = this.control;
272
- this.internals.setFormValue(this.value);
334
+ const parsed = this.numeric;
335
+ this.internals.setFormValue(this.submitted(parsed), this.value);
273
336
  if (!control) {
274
337
  return;
275
338
  }
276
- const parsed = this.numeric;
277
339
  if (this.readOnly) {
278
340
  this.internals.setValidity({});
279
341
  }
@@ -329,23 +391,39 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
329
391
  get blocksImplicitSubmission() {
330
392
  return true;
331
393
  }
394
+ renderDecrementIcon() {
395
+ return html `<slot name="decrement-icon" aria-hidden="true"
396
+ ><svg part="decrement-mark" fill="currentColor" viewBox=${minus.viewBox} aria-hidden="true">
397
+ ${unsafeSVG(minus.content)}
398
+ </svg></slot
399
+ >`;
400
+ }
401
+ renderIncrementIcon() {
402
+ return html `<slot name="increment-icon" aria-hidden="true"
403
+ ><svg part="increment-mark" fill="currentColor" viewBox=${plus.viewBox} aria-hidden="true">
404
+ ${unsafeSVG(plus.content)}
405
+ </svg></slot
406
+ >`;
407
+ }
332
408
  render() {
333
409
  const parsed = this.numeric;
334
410
  const atMin = parsed !== null && this.min !== undefined && parsed <= this.min;
335
411
  const atMax = parsed !== null && this.max !== undefined && parsed >= this.max;
336
412
  return html `${this.renderLabel()}
337
- <div part="field">
413
+ <div part="field" role="group">
338
414
  <button
339
415
  part="decrement"
340
416
  type="button"
341
417
  aria-label=${this.decrementLabel}
342
418
  tabindex="-1"
343
419
  ?disabled=${this.isDisabled || this.readOnly || atMin}
344
- @click=${() => this.stepDown()}
420
+ @click=${this.handleStepperClick}
421
+ @lostpointercapture=${this.stopRepeat}
422
+ @pointercancel=${this.stopRepeat}
423
+ @pointerdown=${this.handleStepperDown}
424
+ @pointerup=${this.stopRepeat}
345
425
  >
346
- <svg part="decrement-mark" fill="currentColor" viewBox=${minus.viewBox} aria-hidden="true">
347
- ${unsafeSVG(minus.content)}
348
- </svg>
426
+ ${this.renderDecrementIcon()}
349
427
  </button>
350
428
  <slot name="start"></slot>
351
429
  ${''}
@@ -355,6 +433,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
355
433
  .value=${this.shown}
356
434
  type="text"
357
435
  role=${this.touchApple ? nothing : 'spinbutton'}
436
+ aria-roledescription=${this.touchApple || this.roleLabel === '' ? nothing : this.roleLabel}
358
437
  inputmode="decimal"
359
438
  autocomplete=${this.autocomplete || nothing}
360
439
  name=${this.name || nothing}
@@ -393,11 +472,13 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
393
472
  aria-label=${this.incrementLabel}
394
473
  tabindex="-1"
395
474
  ?disabled=${this.isDisabled || this.readOnly || atMax}
396
- @click=${() => this.stepUp()}
475
+ @click=${this.handleStepperClick}
476
+ @lostpointercapture=${this.stopRepeat}
477
+ @pointercancel=${this.stopRepeat}
478
+ @pointerdown=${this.handleStepperDown}
479
+ @pointerup=${this.stopRepeat}
397
480
  >
398
- <svg part="increment-mark" fill="currentColor" viewBox=${plus.viewBox} aria-hidden="true">
399
- ${unsafeSVG(plus.content)}
400
- </svg>
481
+ ${this.renderIncrementIcon()}
401
482
  </button>
402
483
  </div>
403
484
  ${this.renderHint()}${this.renderFeedback()}`;
@@ -453,6 +534,10 @@ __decorate([
453
534
  __decorate([
454
535
  property({ attribute: 'readonly', reflect: true, type: Boolean })
455
536
  ], HelixHeadlessNumberInput.prototype, "readOnly", void 0);
537
+ __decorate([
538
+ words,
539
+ property({ attribute: 'role-label' })
540
+ ], HelixHeadlessNumberInput.prototype, "roleLabel", void 0);
456
541
  __decorate([
457
542
  property({ reflect: true, type: Boolean })
458
543
  ], HelixHeadlessNumberInput.prototype, "scrub", void 0);
@@ -2,6 +2,24 @@ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../copy-button/index.js';
4
4
  import { type Row } from '../rows.js';
5
+ /** What a `renderValue` function is told about the entry it draws. */
6
+ export interface ObjectInspectorEntry {
7
+ /** How deep the entry is, from 0 at the root. */
8
+ depth: number;
9
+ /** The property name or array index the value is under, absent for the root. */
10
+ key: string | undefined;
11
+ /** The entry's path, as `active` writes it: `/meta/tags/0`. */
12
+ path: string;
13
+ /** What kind of value it is, as JSON names it: `array`, `boolean`, `null`, `number`, `object` or `string`. Anything else is `other`. */
14
+ type: 'array' | 'boolean' | 'null' | 'number' | 'object' | 'other' | 'string';
15
+ }
16
+ /**
17
+ * Draws one entry's value, for an inspector given `renderValue`.
18
+ *
19
+ * Returns anything Lit renders, such as a template from `html`, a string, or a
20
+ * DOM node, or `undefined` to draw the value as the inspector does.
21
+ */
22
+ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEntry) => unknown;
5
23
  /**
6
24
  * @summary A structured value shown as a tree, where you can close branches,
7
25
  * see how many entries each holds, and copy any part.
@@ -65,6 +83,12 @@ import { type Row } from '../rows.js';
65
83
  * selected. The button sits beside the tree, because assistive technology
66
84
  * cannot reliably reach a button inside a tree item.
67
85
  *
86
+ * **Your own drawing of a value.** `renderValue` is a function from a value and
87
+ * its entry, its path, key, depth and type, to what Lit renders, such as a
88
+ * link for a URL or a swatch for a color. Return `undefined` to draw the value
89
+ * as the inspector does. What you draw sits in the `value` part, after the
90
+ * key. Filtering and copying still read the value itself.
91
+ *
68
92
  * `windowed` draws only the rows in view, for values with thousands of entries.
69
93
  * The box needs a height for windowing to work. The styled tier sets one, and
70
94
  * in the headless tier you set it yourself.
@@ -119,6 +143,7 @@ import { type Row } from '../rows.js';
119
143
  * @csspart summary - The brackets that stand for a branch's contents.
120
144
  * @csspart toggle - The caret on a branch. It points down when the branch is open and sideways when it is closed.
121
145
  * @csspart tree - The tree grid that holds the rows.
146
+ * @csspart value - What `renderValue` drew for an entry, in place of its own drawing.
122
147
  *
123
148
  * @cssproperty --object-inspector-level - How deep a row is, from 0 at the root, for indenting it.
124
149
  */
@@ -229,6 +254,16 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
229
254
  * reach a value.
230
255
  */
231
256
  noCopy: boolean;
257
+ /**
258
+ * Draws an entry's value your way, given the value and its entry. Set it as
259
+ * a property.
260
+ *
261
+ * It returns what Lit renders: a template from `html`, a string, or a DOM
262
+ * node. `undefined` draws the value as the inspector does, so a function can
263
+ * draw some entries and leave the rest, such as only the strings under
264
+ * `/links`.
265
+ */
266
+ renderValue: ObjectInspectorRenderer | undefined;
232
267
  /**
233
268
  * What a screen reader hears when a filter matches one entry, where
234
269
  * `results-label` should not say it. `{count}` is the count.
@@ -397,7 +432,7 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
397
432
  private readonly handleFilterInput;
398
433
  /** Down from the filter field goes to the active row, where the matches are. */
399
434
  private readonly handleFilterKeydown;
400
- private renderValue;
435
+ private drawValue;
401
436
  /**
402
437
  * One entry, named for its state where it holds one.
403
438
  *
@@ -14,6 +14,18 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
14
14
  import { html as staticHtml } from 'lit/static-html.js';
15
15
  import '../../copy-button/index.js';
16
16
  import { cursorFor, entries, flatten, search, seed, siblingBranches, withAncestors, } from '../rows.js';
17
+ const kindOf = (value) => {
18
+ if (value === null) {
19
+ return 'null';
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return 'array';
23
+ }
24
+ const type = typeof value;
25
+ return type === 'boolean' || type === 'number' || type === 'object' || type === 'string'
26
+ ? type
27
+ : 'other';
28
+ };
17
29
  let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends HelixElement {
18
30
  constructor() {
19
31
  super(...arguments);
@@ -393,8 +405,17 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
393
405
  this.toggle(row);
394
406
  }
395
407
  }
396
- renderValue(row) {
408
+ drawValue(row) {
397
409
  const { value } = row;
410
+ const drawn = this.renderValue?.(value, {
411
+ depth: row.depth,
412
+ key: row.key,
413
+ path: row.path,
414
+ type: kindOf(value),
415
+ });
416
+ if (drawn !== undefined) {
417
+ return html `<span part="value">${drawn}</span>`;
418
+ }
398
419
  if (row.branch) {
399
420
  const count = entries(value).length;
400
421
  const array = Array.isArray(value);
@@ -439,7 +460,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
439
460
  >
440
461
  ${unsafeSVG((row.open ? caretDown : caretRight).content)}
441
462
  </svg>`
442
- : nothing}${row.key === undefined ? nothing : html `<span part="key">${row.key}</span>: `}${this.renderValue(row)}</div>
463
+ : nothing}${row.key === undefined ? nothing : html `<span part="key">${row.key}</span>: `}${this.drawValue(row)}</div>
443
464
  </div>`;
444
465
  }
445
466
  hasFocusedRow() {
@@ -563,6 +584,9 @@ __decorate([
563
584
  __decorate([
564
585
  property({ attribute: 'no-copy', type: Boolean })
565
586
  ], HelixHeadlessObjectInspector.prototype, "noCopy", void 0);
587
+ __decorate([
588
+ property({ attribute: false })
589
+ ], HelixHeadlessObjectInspector.prototype, "renderValue", void 0);
566
590
  __decorate([
567
591
  property({ attribute: 'result-label' })
568
592
  ], HelixHeadlessObjectInspector.prototype, "resultLabel", void 0);
@@ -37,6 +37,12 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
37
37
  * An option with `href` is a link: choosing it follows the link and selects
38
38
  * nothing, as an entry in a command palette that goes to a page does.
39
39
  *
40
+ * The option's name is its label alone, as React Aria's `slot="label"` makes
41
+ * it: what is in `start` and `end`, an avatar's initials or a shortcut, is
42
+ * drawn and not read as part of the name. The description is read as the
43
+ * option's description. A name you give with `aria-label` or
44
+ * `aria-labelledby` wins over both.
45
+ *
40
46
  * @since 0.1.0
41
47
  * @status stable
42
48
  * @category forms
@@ -61,6 +67,8 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
61
67
  * @cssstate active - While the keyboard is on the option, which is the option `aria-activedescendant` names.
62
68
  * @cssstate disabled - While the option cannot be chosen.
63
69
  * @cssstate has-indicator - An icon is slotted into `indicator`.
70
+ * @cssstate hovered - While a mouse or a pen is over the option. A touch sets no hover, as React Aria's `data-hovered` does not.
71
+ * @cssstate pressed - While a pointer presses the option, from the press to the release.
64
72
  * @cssstate selected - While the option is chosen.
65
73
  */
66
74
  export declare class HelixHeadlessOption extends HelixElement {
@@ -188,6 +196,17 @@ export declare class HelixHeadlessOption extends HelixElement {
188
196
  * nothing for every choice.
189
197
  */
190
198
  label: string;
199
+ /**
200
+ * The page of commands this option opens, by name, where choosing it would
201
+ * otherwise run it.
202
+ *
203
+ * Declared here and acted on by whatever owns the list, as `alternate` is.
204
+ * `Command` is the only owner that reads it: choosing the option shows the
205
+ * commands slotted as `page:` and the name, such as `slot="page:theme"`,
206
+ * and Backspace in an empty filter goes back. Empty is an option that opens
207
+ * nothing, which is nearly every option.
208
+ */
209
+ opens: string;
191
210
  /**
192
211
  * The link's `rel`. Set it whenever you set `target="_blank"`.
193
212
  */
@@ -291,6 +310,19 @@ export declare class HelixHeadlessOption extends HelixElement {
291
310
  * consumer's markup means. What stops is the write back.
292
311
  */
293
312
  protected willUpdate(changed: PropertyValues): void;
313
+ /**
314
+ * The name is the label part and the description the description part, by
315
+ * reference, because the host's own content runs `start` and `end` into its
316
+ * name: an option with initials and a shortcut was called `P Priya Shah ⌘P`.
317
+ * The alternate destination is part of the description where there is a
318
+ * caption to read with it. A name the consumer wrote on the host wins.
319
+ */
320
+ protected updated(changed: PropertyValues): void;
321
+ /** A hover from a mouse or a pen, which a touch does not make. */
322
+ private readonly handlePointerEnter;
323
+ private readonly handlePointerLeave;
324
+ private readonly handlePointerDown;
325
+ private readonly handlePointerUp;
294
326
  protected render(): TemplateResult;
295
327
  }
296
328
  declare global {
@@ -4,8 +4,8 @@ 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 { customElement, followLink, HelixElement, HelixSelectEvent, slotFilled, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
7
+ import { customElement, followLink, HelixElement, HelixSelectEvent, hostAria, slotFilled, } from '@fusion.dev/helix-core';
8
+ import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  export const optionsAmong = (elements) => {
11
11
  const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
@@ -21,12 +21,26 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
21
21
  this.download = false;
22
22
  this.href = '';
23
23
  this.label = '';
24
+ this.opens = '';
24
25
  this.rel = '';
25
26
  this.selected = false;
26
27
  this.selectionFollowsActive = false;
27
28
  this.target = '';
28
29
  this.textValue = '';
29
30
  this.value = '';
31
+ this.handlePointerEnter = (event) => {
32
+ this.toggleState('hovered', event.pointerType !== 'touch');
33
+ };
34
+ this.handlePointerLeave = () => {
35
+ this.toggleState('hovered', false);
36
+ this.toggleState('pressed', false);
37
+ };
38
+ this.handlePointerDown = (event) => {
39
+ this.toggleState('pressed', event.button === 0 && !this.disabled);
40
+ };
41
+ this.handlePointerUp = () => {
42
+ this.toggleState('pressed', false);
43
+ };
30
44
  }
31
45
  get defaultSelected() {
32
46
  return this.hasAttribute('selected');
@@ -62,6 +76,21 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
62
76
  this.toggleState('active', this.active);
63
77
  this.toggleState('disabled', this.disabled);
64
78
  }
79
+ updated(changed) {
80
+ super.updated(changed);
81
+ const named = this.hasAttribute('aria-label') || this.hasAttribute('aria-labelledby');
82
+ const label = this.partNamed('label');
83
+ const caption = this.partNamed('description');
84
+ const alternate = this.renderRoot.querySelector('[data-alternate]');
85
+ const described = [
86
+ ...(caption ? [caption] : this.knownChildren.filter((child) => child.slot === 'description')),
87
+ ...(alternate ? [alternate] : []),
88
+ ];
89
+ hostAria(this.internals, {
90
+ describedBy: described,
91
+ labelledBy: named || !label ? [] : [label],
92
+ });
93
+ }
65
94
  render() {
66
95
  this.internals.role = 'option';
67
96
  this.internals.ariaSelected =
@@ -69,19 +98,26 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
69
98
  ? String(this.selected || (this.selectionFollowsActive && this.active))
70
99
  : null;
71
100
  this.internals.ariaDisabled = this.disabled ? 'true' : null;
72
- this.internals.ariaDescription =
73
- this.alternate !== '' && this.alternateLabel !== '' ? this.alternateLabel : null;
101
+ const alternate = this.alternate !== '' && this.alternateLabel !== '';
102
+ const captioned = Boolean(this.description) || this.knownChildren.some((child) => child.slot === 'description');
103
+ this.internals.ariaDescription = alternate && !captioned ? this.alternateLabel : null;
74
104
  return html `<div
75
105
  part="item${this.selected ? ' item-selected' : ''}${this.active ? ' item-active' : ''}"
76
106
  @click=${(event) => this.select(event)}
107
+ @pointercancel=${this.handlePointerUp}
108
+ @pointerdown=${this.handlePointerDown}
109
+ @pointerenter=${this.handlePointerEnter}
110
+ @pointerleave=${this.handlePointerLeave}
111
+ @pointerup=${this.handlePointerUp}
77
112
  >
78
113
  <slot name="start"></slot>
79
114
  <span part="text">
80
115
  <span part="label"><slot>${this.label}</slot></span>
81
116
  ${this.description
82
117
  ? html `<span part="description"><slot name="description">${this.description}</slot></span>`
83
- : html `<slot name="description"></slot>`}
118
+ : html `<slot name="description" @slotchange=${() => this.requestUpdate()}></slot>`}
84
119
  </span>
120
+ ${alternate && captioned ? html `<span data-alternate hidden>${this.alternateLabel}</span>` : nothing}
85
121
  <slot name="end"></slot>
86
122
  <slot
87
123
  name="indicator"
@@ -114,6 +150,9 @@ __decorate([
114
150
  __decorate([
115
151
  property()
116
152
  ], HelixHeadlessOption.prototype, "label", void 0);
153
+ __decorate([
154
+ property()
155
+ ], HelixHeadlessOption.prototype, "opens", void 0);
117
156
  __decorate([
118
157
  property()
119
158
  ], HelixHeadlessOption.prototype, "rel", void 0);
@@ -48,11 +48,15 @@ import { type Branch } from '../levels.js';
48
48
  *
49
49
  * @slot - The links, as anchors with fragment hrefs, or lists holding them.
50
50
  * @slot heading - A heading above the links, such as "On this page". It names the navigation.
51
+ * @slot indicator - A mark of your own, drawn inside the `indicator` part, where `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and `--indicator-block-size` reach it.
51
52
  *
52
53
  * @event {HelixActivateEvent} helix-activate - Fired when the section in view changes, with its id as the value. It reports the scroll position and cannot be prevented.
53
54
  *
54
55
  * @csspart heading - The box holding the `heading` slot, above the navigation.
55
- * @csspart indicator - The mark that moves to the section in view.
56
+ * @csspart indicator - The mark that moves to the link of the section in view. It carries that link's
57
+ * place and size as `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and
58
+ * `--indicator-block-size`, and `data-placed` once it has been placed, for a mark you draw
59
+ * and animate yourself.
56
60
  * @csspart nav - The navigation landmark.
57
61
  */
58
62
  export declare class HelixHeadlessOutline extends HelixElement {
@@ -150,7 +154,7 @@ export declare class HelixHeadlessOutline extends HelixElement {
150
154
  * nothing was announced, and nothing said why.
151
155
  */
152
156
  private get links();
153
- /** The children that are the content, which is all of them but the heading. */
157
+ /** The children that are the content, which is all of them but the heading and the mark. */
154
158
  private get content();
155
159
  /**
156
160
  * The links of a flat run, which the render puts into lists, or `undefined`
@@ -8,6 +8,7 @@ import { customElement, elementById, HelixActivateEvent, HelixElement, Indicator
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { levelOf, nest } from '../levels.js';
11
+ const OWN_SLOTS = new Set(['heading', 'indicator']);
11
12
  const isFragment = (element) => element instanceof HTMLAnchorElement && element.getAttribute('href')?.startsWith('#') === true;
12
13
  let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
13
14
  constructor() {
@@ -97,7 +98,7 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
97
98
  : [...element.querySelectorAll('a[href^="#"]')].filter(isFragment));
98
99
  }
99
100
  get content() {
100
- return this.knownChildren.filter((child) => child.slot !== 'heading');
101
+ return this.knownChildren.filter((child) => !OWN_SLOTS.has(child.slot));
101
102
  }
102
103
  get run() {
103
104
  const content = this.content;
@@ -238,6 +239,9 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
238
239
  const root = this.renderRoot;
239
240
  const heading = root?.querySelector?.('slot[name="heading"]');
240
241
  heading?.assign?.(...this.knownChildren.filter((child) => child.slot === 'heading'));
242
+ root
243
+ ?.querySelector?.('slot[name="indicator"]')
244
+ ?.assign?.(...this.knownChildren.filter((child) => child.slot === 'indicator'));
241
245
  const run = this.run;
242
246
  if (run) {
243
247
  for (const slot of root?.querySelectorAll?.('slot[data-link]') ?? []) {
@@ -249,7 +253,7 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
249
253
  return;
250
254
  }
251
255
  const plain = root?.querySelector?.('nav > slot:not([role])');
252
- plain?.assign?.(...[...this.childNodes].filter((node) => node instanceof Element ? node.slot !== 'heading' : node instanceof Text));
256
+ plain?.assign?.(...[...this.childNodes].filter((node) => node instanceof Element ? !OWN_SLOTS.has(node.slot) : node instanceof Text));
253
257
  }
254
258
  updated(changed) {
255
259
  super.updated(changed);
@@ -269,7 +273,7 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
269
273
  return html `<div part="heading"><slot name="heading" @slotchange=${this.nameByHeading}></slot></div>
270
274
  <nav part="nav" aria-label=${this.label} @slotchange=${this.handleSlotChange}>
271
275
  ${run ? this.renderList(nest(run.map((link) => levelOf(link.getAttribute('data-level'))))) : html `<slot></slot>`}
272
- <span part="indicator" aria-hidden="true"></span>
276
+ <span part="indicator" aria-hidden="true"><slot name="indicator"></slot></span>
273
277
  </nav>`;
274
278
  }
275
279
  renderList(branches) {
@@ -96,6 +96,11 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
96
96
  * | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
97
97
  * | Tab | Leaves the table. |
98
98
  *
99
+ * When the rows did not load, set `error-text`, such as "The orders did not
100
+ * load", and put a button that tries again in the `error` slot. The line shows
101
+ * after the rows in place of the empty and loading lines, the live region says
102
+ * it, and the table takes the `error` state. Clear it once you try again.
103
+ *
99
104
  * The CSS custom properties listed for this component are written by it and
100
105
  * read by the stylesheet that draws it. Do not set them. Each holds a number
101
106
  * the component computes and CSS cannot, so the stylesheet can draw the table
@@ -109,6 +114,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
109
114
  *
110
115
  * @slot - The table's rows.
111
116
  * @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.
117
+ * @slot error - Content shown while `error-text` is set, such as a button that tries again. Falls back to `error-text`, which the live region still says.
112
118
  * @slot heading - Words that name the table, shown above it, such as a heading.
113
119
  * @slot loading - Content shown while the rows load, such as a spinner. Falls back to `loading-text`, which the live region still says.
114
120
  *
@@ -132,6 +138,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
132
138
  * @csspart checkbox-mark-indeterminate - Added to `checkbox-mark` while only some rows are selected.
133
139
  * @csspart drag-preview - A copy of the row a pointer drags by its grip, which follows the pointer in the top layer until the row is let go, in a `reorderable` table. Hidden from assistive technology.
134
140
  * @csspart empty - The text shown when there are no rows.
141
+ * @csspart error - The line shown after the rows while `error-text` is set, in place of the empty and loading text.
135
142
  * @csspart grid-end - An empty box after the rows, in a `grid` table. Tab focuses it on its way out of the table, so the key leaves from the end.
136
143
  * @csspart grid-start - An empty box before the rows, in a `grid` table. Shift with Tab focuses it on its way out of the table.
137
144
  * @csspart grip-head - The heading over the row grips, in a `reorderable` table.
@@ -142,6 +149,9 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
142
149
  * @csspart table - The grid.
143
150
  *
144
151
  * @cssproperty --table-template - The column track list the table works out from its columns. The component writes it inline on the element that reads it, so setting it has no effect. Set `columns` to change the tracks.
152
+ *
153
+ * @cssstate empty - While there are no rows and none are loading.
154
+ * @cssstate error - While `error-text` is set.
145
155
  */
146
156
  export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
147
157
  /**
@@ -214,6 +224,16 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
214
224
  descendingLabel: string;
215
225
  /** What screen readers hear when a held row is dropped, in a `reorderable` table. */
216
226
  droppedLabel: string;
227
+ /**
228
+ * The text shown and announced when the rows could not load. Set it when a
229
+ * fetch fails, and clear it once you try again. Empty, the default, shows
230
+ * nothing.
231
+ *
232
+ * While it is set, a line after the rows shows it, or your own content in the
233
+ * `error` slot, in place of the empty and loading lines, and the live region
234
+ * says it.
235
+ */
236
+ errorText: string;
217
237
  /**
218
238
  * The table's name where neither `label` nor an `aria-label` or
219
239
  * `aria-labelledby` on the host gives it one.
@@ -44,7 +44,7 @@ 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']);
47
+ const STATUS = new Set(['empty', 'error', 'loading']);
48
48
  const NAMED = new Set([...STATUS, 'heading']);
49
49
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
50
50
  constructor() {
@@ -57,6 +57,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
57
57
  this.defaultSort = '';
58
58
  this.descendingLabel = 'descending';
59
59
  this.droppedLabel = 'Dropped';
60
+ this.errorText = '';
60
61
  this.fallbackLabel = 'Table';
61
62
  this.grid = false;
62
63
  this.label = '';
@@ -496,6 +497,8 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
496
497
  }
497
498
  updated(changed) {
498
499
  super.updated(changed);
500
+ this.toggleState('error', this.errorText !== '');
501
+ this.toggleState('empty', !this.loading && !this.hasRows);
499
502
  this.assign();
500
503
  this.alignCells();
501
504
  this.markSorted();
@@ -561,8 +564,12 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
561
564
  this.handleSlotChange();
562
565
  }}
563
566
  ></slot>
564
- ${this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')}
565
- </div>${end}${this.renderLive(this.hasRows)}${this.reorder.drawDragPreview()}`;
567
+ ${this.errorText === ''
568
+ ? this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')
569
+ : html `<p part="error" role="row"><span role=${grid ? 'gridcell' : 'cell'}><slot name="error">${this.errorText}</slot></span></p>`}
570
+ </div>${end}${this.errorText === ''
571
+ ? this.renderLive(this.hasRows)
572
+ : html `<span part="live" role="status">${this.errorText}</span>`}${this.reorder.drawDragPreview()}`;
566
573
  }
567
574
  };
568
575
  __decorate([
@@ -590,6 +597,9 @@ __decorate([
590
597
  words,
591
598
  property({ attribute: 'dropped-label' })
592
599
  ], HelixHeadlessTable.prototype, "droppedLabel", void 0);
600
+ __decorate([
601
+ property({ attribute: 'error-text' })
602
+ ], HelixHeadlessTable.prototype, "errorText", void 0);
593
603
  __decorate([
594
604
  words,
595
605
  property({ attribute: 'fallback-label' })