@fusion.dev/helix-headless 0.154.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 (104) hide show
  1. package/custom-elements.json +3213 -179
  2. package/dist/accordion/item/accordion-item.d.ts +15 -0
  3. package/dist/accordion/item/accordion-item.js +12 -1
  4. package/dist/accordion/root/accordion-root.d.ts +2 -1
  5. package/dist/accordion/root/accordion-root.js +1 -0
  6. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  7. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  8. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  9. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  10. package/dist/button-group/root/button-group-root.d.ts +2 -0
  11. package/dist/button-group/root/button-group-root.js +1 -0
  12. package/dist/canvas/node/canvas-node.js +1 -1
  13. package/dist/canvas/root/canvas-root.d.ts +17 -2
  14. package/dist/canvas/root/canvas-root.js +23 -6
  15. package/dist/chat/composer/chat-composer.js +1 -1
  16. package/dist/chat/root/chat-root.d.ts +14 -1
  17. package/dist/chat/root/chat-root.js +14 -4
  18. package/dist/color-input/root/color-input-root.d.ts +9 -3
  19. package/dist/color-input/root/color-input-root.js +3 -1
  20. package/dist/color-picker/channels.d.ts +5 -3
  21. package/dist/color-picker/channels.js +79 -21
  22. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  23. package/dist/color-picker/root/color-picker-root.js +75 -13
  24. package/dist/combobox/root/combobox-root.d.ts +10 -0
  25. package/dist/combobox/root/combobox-root.js +18 -3
  26. package/dist/command/root/command-root.d.ts +116 -1
  27. package/dist/command/root/command-root.js +124 -15
  28. package/dist/data-table/root/data-table-root.d.ts +25 -0
  29. package/dist/data-table/root/data-table-root.js +13 -3
  30. package/dist/date-input/root/date-input-root.d.ts +33 -1
  31. package/dist/date-input/root/date-input-root.js +9 -2
  32. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  33. package/dist/date-picker/root/date-picker-root.js +54 -31
  34. package/dist/element-events.generated.js +3 -1
  35. package/dist/graph/root/graph-root.d.ts +13 -0
  36. package/dist/graph/root/graph-root.js +16 -0
  37. package/dist/list/root/list-root.d.ts +74 -1
  38. package/dist/list/root/list-root.js +92 -4
  39. package/dist/list/selection.js +3 -3
  40. package/dist/map/root/map-root.d.ts +6 -0
  41. package/dist/map/root/map-root.js +8 -1
  42. package/dist/navigation/root/navigation-root.d.ts +26 -0
  43. package/dist/navigation/root/navigation-root.js +58 -1
  44. package/dist/number-input/root/number-input-root.d.ts +63 -6
  45. package/dist/number-input/root/number-input-root.js +96 -11
  46. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  47. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  48. package/dist/option/root/option-root.d.ts +32 -0
  49. package/dist/option/root/option-root.js +44 -5
  50. package/dist/outline/root/outline-root.d.ts +6 -2
  51. package/dist/outline/root/outline-root.js +7 -3
  52. package/dist/select/root/select-root.d.ts +29 -3
  53. package/dist/select/root/select-root.js +34 -9
  54. package/dist/slider/root/slider-root.d.ts +71 -9
  55. package/dist/slider/root/slider-root.js +129 -42
  56. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  57. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  58. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  59. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  60. package/dist/splitter/root/splitter-root.d.ts +22 -0
  61. package/dist/splitter/root/splitter-root.js +36 -7
  62. package/dist/steps/reach.d.ts +5 -1
  63. package/dist/steps/reach.js +2 -2
  64. package/dist/steps/root/steps-root.d.ts +37 -2
  65. package/dist/steps/root/steps-root.js +49 -7
  66. package/dist/steps/step/step.d.ts +6 -0
  67. package/dist/steps/step/step.js +4 -0
  68. package/dist/table/root/table-root.d.ts +20 -0
  69. package/dist/table/root/table-root.js +13 -3
  70. package/dist/tabs/root/tabs-root.d.ts +37 -0
  71. package/dist/tabs/root/tabs-root.js +92 -2
  72. package/dist/tabs/tab/tab.d.ts +38 -2
  73. package/dist/tabs/tab/tab.js +45 -4
  74. package/dist/tag/group/tag-group.d.ts +29 -3
  75. package/dist/tag/group/tag-group.js +42 -11
  76. package/dist/time-input/root/time-input-root.d.ts +32 -1
  77. package/dist/time-input/root/time-input-root.js +9 -2
  78. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  79. package/dist/time-picker/root/time-picker-root.js +58 -7
  80. package/dist/timeline/item/timeline-item.d.ts +25 -0
  81. package/dist/timeline/item/timeline-item.js +27 -2
  82. package/dist/toggle/root/toggle-root.d.ts +2 -0
  83. package/dist/toggle/root/toggle-root.js +1 -1
  84. package/dist/tree/item/tree-item.d.ts +10 -0
  85. package/dist/tree/item/tree-item.js +12 -0
  86. package/dist/tree/root/tree-root.d.ts +40 -1
  87. package/dist/tree/root/tree-root.js +120 -4
  88. package/dist/tree/selection.d.ts +6 -0
  89. package/dist/tree/selection.js +11 -0
  90. package/dist/video/root/keys.d.ts +6 -2
  91. package/dist/video/root/keys.js +18 -1
  92. package/dist/video/root/video-root.d.ts +60 -1
  93. package/dist/video/root/video-root.js +70 -2
  94. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  95. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  96. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  97. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  98. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  99. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  100. package/lint.json +1 -1
  101. package/package.json +5 -5
  102. package/types/jsx.d.ts +3885 -383
  103. package/types/vue.d.ts +1045 -107
  104. package/types/words.d.ts +146 -1
@@ -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) {
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, type DataOptionItem, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { type HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  import '../../listbox/index.js';
@@ -13,7 +13,10 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
13
13
  * place for focus to return to when the list closes.
14
14
  *
15
15
  * Your options stay in your light DOM and are slotted through into the listbox,
16
- * so the listbox looks through slots to find them.
16
+ * so the listbox looks through slots to find them. For a list too long to draw
17
+ * whole, set `items` instead: the list draws only the options in sight, as
18
+ * Listbox's `items` does, and the trigger, its keys and the form read the
19
+ * whole set.
17
20
  *
18
21
  * `Select` owns the value, passes it down to the listbox, and takes changes
19
22
  * back, so the value lives in one place.
@@ -102,6 +105,8 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
102
105
  *
103
106
  * @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
104
107
  * @cssstate open - While the list is open.
108
+ * @cssstate placed-bottom - While the open list sits below the trigger.
109
+ * @cssstate placed-top - While the open list sits above the trigger, where there was no room below.
105
110
  */
106
111
  export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
107
112
  /**
@@ -145,6 +150,20 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
145
150
  * anything a consumer sets.
146
151
  */
147
152
  focusable: boolean;
153
+ /**
154
+ * The options as data, for a list too long to draw whole: each a value,
155
+ * which is also its words, or a record with a `value`, a `label` and
156
+ * `disabled`. Passed to the list, which draws only the options in sight,
157
+ * as Listbox's `items` does. Set, the options you write as children are
158
+ * not read.
159
+ *
160
+ * The trigger shows the chosen options' labels, the letters and the Left
161
+ * and Right keys on the closed trigger choose over the whole set, and a
162
+ * chosen value whose option leaves `items` drops out of `value`.
163
+ * `value-content` is not called for `items`. Import the Option family as
164
+ * well, whose elements are drawn.
165
+ */
166
+ items: readonly DataOptionItem[] | undefined;
148
167
  /**
149
168
  * The open list's name where nothing names the Select, read by screen
150
169
  * readers. The list has no visible name.
@@ -270,8 +289,15 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
270
289
  * the commit creates the text node it needs.
271
290
  */
272
291
  private get display();
273
- /** The chosen options, in the order they appear. None on a server or while hydrating. */
292
+ /** The chosen options, in the order they appear. None on a server or while hydrating, and none for `items`. */
274
293
  private get chosenOptions();
294
+ /** `items` as the list reads them, built once per array. */
295
+ private data;
296
+ /**
297
+ * Every option the select reads, in order: the records of `items`, or the
298
+ * option elements among its children.
299
+ */
300
+ private get entries();
275
301
  /**
276
302
  * What the trigger shows, behind a seam: what `value-content` draws for the
277
303
  * chosen options, or their words, or the placeholder.