@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
@@ -11,8 +11,10 @@ import { HelixHeadlessTag } from '../root/tag-root.js';
11
11
  let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.disallowEmptySelection = false;
14
15
  this.emptyLabel = 'No tags';
15
16
  this.label = '';
17
+ this.selectionBehavior = 'toggle';
16
18
  this.value = '';
17
19
  this.keys = new KeyboardNavigationController(this, {
18
20
  items: () => this.reachable.map((tag) => this.navigable(tag)),
@@ -34,6 +36,13 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
34
36
  }
35
37
  return;
36
38
  }
39
+ if ((event.ctrlKey || event.metaKey) && event.key === ' ') {
40
+ if (this.replaces && multiple) {
41
+ event.preventDefault();
42
+ this.press(tag, event);
43
+ }
44
+ return;
45
+ }
37
46
  if (event.ctrlKey || event.metaKey) {
38
47
  return;
39
48
  }
@@ -45,6 +54,9 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
45
54
  else if (!event.shiftKey) {
46
55
  this.anchor = to;
47
56
  this.reached = to;
57
+ if (this.replaces && to && !to.disabled) {
58
+ this.ask(this.valueFor(to));
59
+ }
48
60
  }
49
61
  return;
50
62
  }
@@ -62,7 +74,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
62
74
  }
63
75
  if ((event.key === ' ' || event.key === 'Enter') && this.selectionMode) {
64
76
  event.preventDefault();
65
- this.press(tag);
77
+ this.press(tag, event);
66
78
  }
67
79
  };
68
80
  this.handleClick = (event) => {
@@ -72,7 +84,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
72
84
  .some((node) => node instanceof Element &&
73
85
  /(^| )(dismiss|link)( |$)/.test(node.getAttribute('part') ?? ''));
74
86
  if (tag && !control) {
75
- this.press(tag);
87
+ this.press(tag, event);
76
88
  }
77
89
  };
78
90
  this.handleDismiss = (event) => {
@@ -172,14 +184,33 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
172
184
  .composedPath()
173
185
  .find((node) => node instanceof HelixHeadlessTag);
174
186
  }
175
- press(tag) {
187
+ press(tag, cause) {
176
188
  const mode = this.selectionMode ?? 'none';
177
189
  if (mode === 'none' || tag.disabled) {
178
190
  return;
179
191
  }
192
+ if (mode === 'multiple' && cause?.shiftKey && this.anchor?.isConnected) {
193
+ this.extend(this.anchor, tag);
194
+ return;
195
+ }
180
196
  this.anchor = tag;
181
197
  this.reached = tag;
182
- requestValue(this, pressedValue(mode, this.written, this.valueFor(tag)), (value) => {
198
+ const adds = cause?.ctrlKey === true || cause?.metaKey === true;
199
+ this.ask(this.replaces && !(adds && mode === 'multiple')
200
+ ? this.valueFor(tag)
201
+ : pressedValue(mode, this.written, this.valueFor(tag)));
202
+ }
203
+ get replaces() {
204
+ return this.selectionMode !== undefined && this.selectionBehavior === 'replace';
205
+ }
206
+ ask(next) {
207
+ if (next === this.written) {
208
+ return;
209
+ }
210
+ if (this.disallowEmptySelection && commaList(next).length === 0) {
211
+ return;
212
+ }
213
+ requestValue(this, next, (value) => {
183
214
  this.value = value;
184
215
  });
185
216
  }
@@ -199,13 +230,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
199
230
  this.choose(this.tags.filter((tag) => chosen.includes(tag)));
200
231
  }
201
232
  choose(tags) {
202
- const next = [...new Set(tags.map((tag) => this.valueFor(tag)))].join(', ');
203
- if (next === this.written) {
204
- return;
205
- }
206
- requestValue(this, next, (value) => {
207
- this.value = value;
208
- });
233
+ this.ask([...new Set(tags.map((tag) => this.valueFor(tag)))].join(', '));
209
234
  }
210
235
  connectedCallback() {
211
236
  super.connectedCallback();
@@ -241,6 +266,9 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
241
266
  return html `<slot @slotchange=${this.handleSlotchange}></slot>${empty}`;
242
267
  }
243
268
  };
269
+ __decorate([
270
+ property({ attribute: 'disallow-empty-selection', type: Boolean })
271
+ ], HelixHeadlessTagGroup.prototype, "disallowEmptySelection", void 0);
244
272
  __decorate([
245
273
  words,
246
274
  property({ attribute: 'empty-label' })
@@ -248,6 +276,9 @@ __decorate([
248
276
  __decorate([
249
277
  property()
250
278
  ], HelixHeadlessTagGroup.prototype, "label", void 0);
279
+ __decorate([
280
+ property({ attribute: 'selection-behavior' })
281
+ ], HelixHeadlessTagGroup.prototype, "selectionBehavior", void 0);
251
282
  __decorate([
252
283
  property({ attribute: 'selection-mode' })
253
284
  ], HelixHeadlessTagGroup.prototype, "selectionMode", void 0);
@@ -34,17 +34,41 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
34
34
  * hour, a minute, and AM or PM where the clock has them, each set with digits
35
35
  * or the arrow keys. Alt+Down opens the list from a segment.
36
36
  *
37
+ * Text that is not a time gives the form an empty value, as a native time
38
+ * field does, and `value` keeps the text so it can be corrected.
39
+ *
40
+ * **Keyboard.** The field is one tab stop, or each segment is with
41
+ * `segmented`, and the trigger is one more.
42
+ *
43
+ * | Key | What it does |
44
+ * |---|---|
45
+ * | Down | In the text field, opens the list on the chosen time. |
46
+ * | Alt with Down | Opens the list, from the text field or a segment. |
47
+ * | Enter | In the text field, takes the typed time and submits the form, as a native field does. In the list, chooses the highlighted time. |
48
+ * | Escape | Closes the list and puts focus back in the field. |
49
+ * | Up, Down on a segment | Steps the segment by one, round from its last value to its first. |
50
+ * | Page Up, Page Down on a segment | Steps by a page: two hours, or fifteen minutes or seconds. |
51
+ * | Home, End on a segment | Goes to the segment's first or last value. |
52
+ * | Digits on a segment | Fill it, and move to the next once it can take no more. |
53
+ * | A letter on the half of the day | Picks the half whose name starts with it, such as A for AM. |
54
+ * | Backspace, Delete on a segment | Empty the segment. |
55
+ * | Left, Right on a segment | Move to the previous or the next segment, along the line of text. |
56
+ *
57
+ * In the list, the keys are `TimePicker`'s.
58
+ *
37
59
  * @since 0.1.0
38
60
  * @status stable
39
61
  * @category forms
40
62
  * @aliases TimeField, InputTime
41
63
  * @documentation https://fusion.dev/helix/components/time-input
42
64
  *
65
+ * @slot end - Content after the time, before the trigger, such as a clear button.
43
66
  * @slot feedback - The message that says what is wrong with the value.
44
67
  * @slot hint - Help under the field.
45
68
  * @slot label - The label, for content richer than the `label` text.
46
69
  * @slot panel-footer - Content in the panel under the times, such as a note or an action.
47
70
  * @slot panel-header - Content in the panel above the times, such as a heading or a shortcut.
71
+ * @slot start - Content before the time, such as an icon.
48
72
  * @slot trigger - The icon on the field that shows a list opens.
49
73
  *
50
74
  * @event {Event} change - Fired after the value changes.
@@ -76,6 +100,8 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
76
100
  * @csspart picker-column-search-field - The filter field.
77
101
  * @csspart picker-columns - The row of columns, in `wheels`.
78
102
  * @csspart picker-feedback - The picker's own message, under the times, while the picker's `feedback` is set.
103
+ * @csspart picker-hint - The picker's hint, which Time Input never sets: help under the field is the field's own `hint`.
104
+ * @csspart picker-label - The picker's visible label, which Time Input never draws: the field's `label` names it.
79
105
  * @csspart picker-list - The listbox holding the times, in `list`.
80
106
  * @csspart picker-list-empty - The text shown when the filter hides every option.
81
107
  * @csspart picker-list-error - The list's `error` part, which the list never sets: its times do not load.
@@ -283,7 +309,12 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
283
309
  * what says the reader is done.
284
310
  */
285
311
  private readonly handleWheelEnter;
286
- /** Each time, where `mode` holds several, sent one entry each. */
312
+ /**
313
+ * Each time, where `mode` holds several, sent one entry each.
314
+ *
315
+ * One time that is text and no time, such as `2:45 P`, is sent as an empty
316
+ * entry, as a native time field sends a time it cannot read.
317
+ */
287
318
  protected get answers(): readonly string[] | undefined;
288
319
  /** What is wrong with the times, which the text input cannot say. */
289
320
  protected validate(): string | readonly [keyof ValidityStateFlags, string];
@@ -146,7 +146,11 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
146
146
  return held.length > 0 && held.every((one) => this.isTime(one)) ? held.join(',') : '';
147
147
  }
148
148
  get answers() {
149
- return this.mode === undefined ? undefined : this.held;
149
+ if (this.mode !== undefined) {
150
+ return this.held;
151
+ }
152
+ const text = this.value.trim();
153
+ return text === '' || this.isTime(text) ? undefined : [''];
150
154
  }
151
155
  validate() {
152
156
  const held = this.held;
@@ -197,7 +201,8 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
197
201
  column-filter-mark: picker-column-filter-mark, column-hint: picker-column-hint,
198
202
  column-items: picker-column-items, column-label: picker-column-label,
199
203
  column-live: picker-column-live, column-loading: picker-column-loading, column-search: picker-column-search,
200
- column-search-field: picker-column-search-field, columns: picker-columns, feedback: picker-feedback, list: picker-list,
204
+ column-search-field: picker-column-search-field, columns: picker-columns, feedback: picker-feedback, hint: picker-hint,
205
+ label: picker-label, list: picker-list,
201
206
  list-empty: picker-list-empty, list-error: picker-list-error, list-feedback: picker-list-feedback,
202
207
  list-filter-icon: picker-list-filter-icon, list-filter-mark: picker-list-filter-mark,
203
208
  list-hint: picker-list-hint, list-items: picker-list-items, list-label: picker-list-label,
@@ -269,10 +274,12 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
269
274
  render() {
270
275
  return html `${this.renderLabel()}
271
276
  <div part="field" @keydown=${this.handleKeydown}>
277
+ <slot name="start"></slot>
272
278
  ${this.mode === 'multiple'
273
279
  ? this.renderTokenRow(this.shownTimes.map((at) => this.renderTag(at)), this.restCount)
274
280
  : nothing}
275
281
  ${this.segmentsShown ? this.renderSegments() : this.renderControl()}
282
+ <slot name="end"></slot>
276
283
  <button
277
284
  part="trigger"
278
285
  type="button"
@@ -35,12 +35,37 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
35
35
  * its choice in markup for the same reason. The list and the wheels both do
36
36
  * this, because each wheel is also a listbox.
37
37
  *
38
+ * **Keyboard.** The list is one tab stop, and each wheel is one.
39
+ *
40
+ * | Key | What it does |
41
+ * |---|---|
42
+ * | Down, Up | Moves to the next or the previous time, or in a wheel to the next or the previous hour, minute, second, or half of the day. |
43
+ * | Home, End | Moves to the first or the last time, or the first or the last in a wheel. |
44
+ * | Page Down, Page Up | Moves by as many times as the list shows at once. |
45
+ * | Digits | Moves to the next time that starts with what you typed, such as `9` for 9:00. |
46
+ * | Enter, Space | Chooses the highlighted time. Moving chooses too, except under `manual` or with a `mode`. With a `mode`, Space adds or removes the time. |
47
+ * | Right, Left | In the wheels, moves to the next or the previous wheel, mirrored right to left. |
48
+ * | Tab | Moves to the next wheel, then out of the picker. |
49
+ *
50
+ * **ARIA.** The list is a `listbox` named by `label`, a slotted label, the
51
+ * host's `aria-label` or its `aria-labelledby`. A development build warns
52
+ * where all four are empty. The list is described by the hint, or by the
53
+ * message in `feedback` in its place, and carries `aria-multiselectable` with
54
+ * a `mode` and `aria-invalid` while `feedback` says what is wrong. The wheels
55
+ * are a `group` with that name, holding one `listbox` per wheel, named "Hour",
56
+ * "Minute", "Second" and "AM/PM" in the page's language, and each described
57
+ * the same way.
58
+ *
38
59
  * @since 0.1.0
39
60
  * @status stable
40
61
  * @category forms
41
62
  * @aliases Time, TimeList
42
63
  * @documentation https://fusion.dev/helix/components/time-picker
43
64
  *
65
+ * @slot empty - What the list shows when no time falls between `min` and `max`, for content richer than `empty-text`.
66
+ * @slot hint - Help under the times, for content richer than the `hint` text.
67
+ * @slot label - A visible label above the times, which names the list, for content richer than the `label` text.
68
+ *
44
69
  * @event {Event} change - Fired after the value changes.
45
70
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the time changes, and cancelable. Canceling it keeps the current time.
46
71
  * @event {Event} input - Fired after the value changes.
@@ -60,6 +85,8 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
60
85
  * @csspart column-search-field - The filter field.
61
86
  * @csspart columns - The row of columns, in `wheels`.
62
87
  * @csspart feedback - The message under the times, while `feedback` is set.
88
+ * @csspart hint - Help under the times, shown while there is no message in `feedback`.
89
+ * @csspart label - The visible label above the times, drawn while a label is slotted.
63
90
  * @csspart list - The listbox holding the times, in `list`.
64
91
  * @csspart list-empty - The text shown when the filter hides every option.
65
92
  * @csspart list-error - The list's `error` part, which Time Picker never sets: its times do not load.
@@ -115,6 +142,19 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
115
142
  * reached honestly rather than by handing over an empty range.
116
143
  */
117
144
  disableTimes: string;
145
+ /**
146
+ * What the list says when no time falls between `min` and `max`.
147
+ *
148
+ * The fallback of the `empty` slot.
149
+ */
150
+ emptyText: string;
151
+ /**
152
+ * Help under the times, such as the hours a shop is open, read by screen
153
+ * readers as the list's description. A message in `feedback` replaces it.
154
+ *
155
+ * The fallback of the `hint` slot.
156
+ */
157
+ hint: string;
118
158
  /**
119
159
  * The clock the times are written on: `h12` for twelve hours with a
120
160
  * meridiem, `h23` for twenty-four.
@@ -134,7 +174,8 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
134
174
  /**
135
175
  * Accessible name for the list, which has no visible one of its own.
136
176
  *
137
- * An `aria-label` or `aria-labelledby` on the host names the list instead.
177
+ * An `aria-label` or `aria-labelledby` on the host names the list instead,
178
+ * and so does a label slotted into `label`, which is also drawn above it.
138
179
  */
139
180
  label: string;
140
181
  /**
@@ -473,6 +514,26 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
473
514
  * aria-label="Slot">` was a listbox named "Time".
474
515
  */
475
516
  private get listName();
517
+ /** Whether a hint is slotted, which draws the `hint` part around it. */
518
+ private hintSlotted;
519
+ /** Whether the hint shows: there is one, and no message stands in its place. */
520
+ private get hinted();
521
+ /** The ids that describe each list: the hint, or the message in its place. */
522
+ private get describedBy();
523
+ /**
524
+ * The hint under the times, or a bare slot while there is none to show,
525
+ * hidden while a message stands in its place, so a slotted hint goes too.
526
+ */
527
+ private renderHint;
528
+ /** Whether a label is slotted, which draws the `label` part around it. */
529
+ private labelSlotted;
530
+ /** Reads the `label` slot, and the list's name again with it. */
531
+ private readonly handleLabelSlot;
532
+ /**
533
+ * The slotted label, in the `label` part, above the times. A bare slot while
534
+ * nothing is slotted, which draws no box.
535
+ */
536
+ private renderSlottedLabel;
476
537
  /**
477
538
  * Hears the host's `aria-label` and `aria-labelledby` change, which no
478
539
  * property reports. Made on connection, there being no `MutationObserver` on
@@ -482,6 +543,7 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
482
543
  connectedCallback(): void;
483
544
  disconnectedCallback(): void;
484
545
  protected willUpdate(changed: PropertyValues): void;
546
+ protected firstUpdated(changed: PropertyValues): void;
485
547
  protected updated(changed: PropertyValues): void;
486
548
  /**
487
549
  * Moves focus to the list, or to the first wheel.
@@ -4,9 +4,9 @@ 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 { clockLocale, commaList, customElement, dayPeriod, formatWords, HelixFormElement, hostWords, inlineStep, isTwelveHour, MixinControllerValue, MixinPickerFeedback, nearestTime, offeredTimes, reportValues, secondsAsValue, timeBarred, timeFault, valueAsSeconds, wheelParts, wholeHour, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
7
+ import { auditName, clockLocale, commaList, customElement, dayPeriod, formatWords, HelixFormElement, hostWords, inlineStep, isTwelveHour, MixinControllerValue, MixinPickerFeedback, nearestTime, offeredTimes, reportValues, secondsAsValue, slotFilled, slottedWords, timeBarred, timeFault, valueAsSeconds, wheelParts, wholeHour, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import { wheelName } from '../wheel-name.js';
12
12
  import '../../listbox/index.js';
@@ -16,6 +16,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
16
16
  super(...arguments);
17
17
  this.badInputMessage = '{mode, select, single {Enter a time as {shape}.} other {Enter times as {shape}, separated by commas.}}';
18
18
  this.disableTimes = '';
19
+ this.emptyText = 'No times to choose from';
20
+ this.hint = '';
19
21
  this.label = 'Time';
20
22
  this.locale = '';
21
23
  this.manual = false;
@@ -83,6 +85,12 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
83
85
  this.unavailableMessage = 'Choose a time that is available.';
84
86
  this.valueMissingMessage = 'Choose a time.';
85
87
  this.listRevealed = false;
88
+ this.hintSlotted = false;
89
+ this.labelSlotted = false;
90
+ this.handleLabelSlot = (event) => {
91
+ this.labelSlotted = slotFilled(event.target);
92
+ this.requestUpdate();
93
+ };
86
94
  this.naming = undefined;
87
95
  this.handleColumnsKeydown = (event) => {
88
96
  const step = inlineStep(this, event.key);
@@ -247,7 +255,27 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
247
255
  this.reveal();
248
256
  }
249
257
  get listName() {
250
- return hostWords(this, this.label);
258
+ const slotted = this.labelSlotted ? slottedWords(this, ['label']) : '';
259
+ return hostWords(this, slotted || this.label);
260
+ }
261
+ get hinted() {
262
+ return (this.hint !== '' || this.hintSlotted) && this.feedback === '';
263
+ }
264
+ get describedBy() {
265
+ return this.feedback !== '' ? 'feedback' : this.hinted ? 'hint' : nothing;
266
+ }
267
+ renderHint() {
268
+ const read = (event) => {
269
+ this.hintSlotted = slotFilled(event.target);
270
+ };
271
+ return this.hinted
272
+ ? html `<p part="hint" id="hint"><slot name="hint" @slotchange=${read}>${this.hint}</slot></p>`
273
+ : html `<slot name="hint" ?hidden=${this.feedback !== ''} @slotchange=${read}></slot>`;
274
+ }
275
+ renderSlottedLabel() {
276
+ return this.labelSlotted
277
+ ? html `<div part="label"><slot name="label" @slotchange=${this.handleLabelSlot}></slot></div>`
278
+ : html `<slot name="label" @slotchange=${this.handleLabelSlot}></slot>`;
251
279
  }
252
280
  connectedCallback() {
253
281
  super.connectedCallback();
@@ -264,6 +292,14 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
264
292
  this.report();
265
293
  this.toggleState('blank', this.value === '');
266
294
  }
295
+ firstUpdated(changed) {
296
+ super.firstUpdated(changed);
297
+ auditName(this, () => null, 'Give it a `label`, a label in the `label` slot, or an `aria-label` that says what the time is for.', () => this.listName !== '');
298
+ const list = this.partNamed('list');
299
+ if (list) {
300
+ auditName(list, () => null, '', () => true);
301
+ }
302
+ }
267
303
  updated(changed) {
268
304
  super.updated(changed);
269
305
  this.reportRequired();
@@ -284,7 +320,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
284
320
  return html `<${this.tag('listbox')}
285
321
  part="column"
286
322
  data-column=${part}
287
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
323
+ aria-describedby=${this.describedBy}
288
324
  aria-invalid=${this.marked ? 'true' : nothing}
289
325
  exportparts="empty: column-empty, error: column-error, feedback: column-feedback, filter-icon: column-filter-icon,
290
326
  filter-mark: column-filter-mark, hint: column-hint, items: column-items, label: column-label,
@@ -336,7 +372,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
336
372
  renderList() {
337
373
  return html `<${this.tag('listbox')}
338
374
  part="list"
339
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
375
+ aria-describedby=${this.describedBy}
340
376
  aria-invalid=${this.marked ? 'true' : nothing}
341
377
  exportparts="empty: list-empty, error: list-error, feedback: list-feedback, filter-icon: list-filter-icon,
342
378
  filter-mark: list-filter-mark, hint: list-hint, items: list-items, label: list-label,
@@ -344,6 +380,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
344
380
  aria-label=${this.listName}
345
381
  data-unset=${this.value === '' ? '' : nothing}
346
382
  ?disabled=${this.isDisabled}
383
+ empty-text=${this.emptyText}
347
384
  ?manual=${this.manual}
348
385
  ?multiple=${this.mode !== undefined}
349
386
  .value=${this.value}
@@ -365,13 +402,14 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
365
402
  >${writeClock(at, this.clock, this.seconds)}</${this.tag('option')}
366
403
  >`;
367
404
  })}
405
+ <slot name="empty" slot="empty">${this.emptyText}</slot>
368
406
  </${this.tag('listbox')}>`;
369
407
  }
370
408
  render() {
371
409
  if (this.variant === 'wheels' && this.mode === undefined) {
372
- return html `${this.renderSpinner()}${this.renderPickerFeedback()}`;
410
+ return html `${this.renderSlottedLabel()}${this.renderSpinner()}${this.renderHint()}${this.renderPickerFeedback()}`;
373
411
  }
374
- return html `${this.renderList()}${this.renderPickerFeedback()}`;
412
+ return html `${this.renderSlottedLabel()}${this.renderList()}${this.renderHint()}${this.renderPickerFeedback()}`;
375
413
  }
376
414
  get valueAsDate() {
377
415
  const at = valueAsSeconds(this.value);
@@ -397,6 +435,13 @@ __decorate([
397
435
  __decorate([
398
436
  property({ attribute: 'disable-times' })
399
437
  ], HelixHeadlessTimePicker.prototype, "disableTimes", void 0);
438
+ __decorate([
439
+ words,
440
+ property({ attribute: 'empty-text' })
441
+ ], HelixHeadlessTimePicker.prototype, "emptyText", void 0);
442
+ __decorate([
443
+ property()
444
+ ], HelixHeadlessTimePicker.prototype, "hint", void 0);
400
445
  __decorate([
401
446
  property({ attribute: 'hour-cycle' })
402
447
  ], HelixHeadlessTimePicker.prototype, "hourCycle", void 0);
@@ -466,6 +511,12 @@ __decorate([
466
511
  __decorate([
467
512
  property({ reflect: true })
468
513
  ], HelixHeadlessTimePicker.prototype, "variant", void 0);
514
+ __decorate([
515
+ state()
516
+ ], HelixHeadlessTimePicker.prototype, "hintSlotted", void 0);
517
+ __decorate([
518
+ state()
519
+ ], HelixHeadlessTimePicker.prototype, "labelSlotted", void 0);
469
520
  HelixHeadlessTimePicker = __decorate([
470
521
  customElement('helix-headless-time-picker')
471
522
  ], HelixHeadlessTimePicker);
@@ -8,6 +8,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
8
8
  * machine-readable value such as "2026-08-19" travels with a label such as
9
9
  * "yesterday". Without `datetime`, the label is a `<span>`.
10
10
  *
11
+ * **`relative` writes the time for you** as how long ago it was, such as "3
12
+ * hours ago", from `datetime`, where the `time` slot is empty. The words move
13
+ * on as they go stale, "3 hours ago" becoming "4 hours ago", with one wakeup
14
+ * shared by every reading on the page, as `FormatDate` keeps its own. They are
15
+ * in the language of the page, or of the nearest `lang`.
16
+ *
11
17
  * Each entry draws the connector line that arrives at it and hides it when it
12
18
  * is the first entry, using `:host(:first-child)`. This needs no index, no
13
19
  * counting parent, and no script. `Steps` and `Breadcrumbs` work the same way.
@@ -44,6 +50,25 @@ export declare class HelixHeadlessTimelineItem extends HelixElement {
44
50
  * know what instant it names.
45
51
  */
46
52
  datetime: string;
53
+ /**
54
+ * Writes the time as how long ago it was, or how long until it, from
55
+ * `datetime`, where the `time` slot is empty: `long` ("3 hours ago"),
56
+ * `short` ("3 hr. ago") or `narrow` ("3h ago"). Set with no value, it is
57
+ * `long`. The words move on as they go stale.
58
+ */
59
+ relative?: 'long' | 'narrow' | 'short';
60
+ /** The time the reading is drawn against, moved on when the words go stale. */
61
+ private readonly clock;
62
+ /**
63
+ * What the `time` slot shows where nothing is slotted: the reading under
64
+ * `relative`, read against the clock before each render, and nothing
65
+ * otherwise.
66
+ */
67
+ private reading;
68
+ /** The instant `datetime` names, where `relative` asks for a reading of it. */
69
+ private get moment();
70
+ /** What the reading says at `now`, and when it stops being true. */
71
+ private readingAt;
47
72
  /**
48
73
  * The kind of event. It sets the color of the mark and nothing else.
49
74
  */
@@ -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 { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, RelativeClockController, relativeTime, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { circle, record } from '@fusion.dev/helix-icons';
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -13,9 +13,31 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.datetime = '';
16
+ this.clock = new RelativeClockController(this, {
17
+ expires: (now) => {
18
+ const moment = this.moment;
19
+ return moment ? this.readingAt(moment, now).next : undefined;
20
+ },
21
+ });
22
+ this.reading = '';
23
+ }
24
+ get moment() {
25
+ if (this.relative === undefined || this.datetime === '') {
26
+ return undefined;
27
+ }
28
+ const parsed = new Date(this.datetime);
29
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
30
+ }
31
+ readingAt(moment, now) {
32
+ return relativeTime(wordsLocale(this), { style: this.relative || 'long' })(moment, now);
16
33
  }
17
34
  willUpdate(changed) {
18
35
  super.willUpdate(changed);
36
+ if (['datetime', 'relative'].some((key) => changed.has(key))) {
37
+ this.clock.tick();
38
+ }
39
+ const moment = this.moment;
40
+ this.reading = moment ? this.readingAt(moment, new Date(this.clock.now)).text : '';
19
41
  hostAria(this.internals, { role: 'listitem' });
20
42
  }
21
43
  renderMarker() {
@@ -29,7 +51,7 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
29
51
  >`;
30
52
  }
31
53
  render() {
32
- const when = html `<slot name="time"></slot>`;
54
+ const when = html `<slot name="time">${this.reading}</slot>`;
33
55
  return html `<div part="rail">
34
56
  ${this.renderMarker()}
35
57
  </div>
@@ -46,6 +68,9 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
46
68
  __decorate([
47
69
  property()
48
70
  ], HelixHeadlessTimelineItem.prototype, "datetime", void 0);
71
+ __decorate([
72
+ property()
73
+ ], HelixHeadlessTimelineItem.prototype, "relative", void 0);
49
74
  __decorate([
50
75
  property({ reflect: true })
51
76
  ], HelixHeadlessTimelineItem.prototype, "tone", void 0);
@@ -25,6 +25,8 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
25
25
  * @documentation https://fusion.dev/helix/components/toggle
26
26
  *
27
27
  * @slot - The item's label.
28
+ * @slot end - Content after the label, such as a count or a shortcut.
29
+ * @slot start - Content before the label, such as an icon.
28
30
  *
29
31
  * @event {Event} change - Fired after a toggle button on its own is pressed or released.
30
32
  * @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: call `preventDefault()` to keep the current choice.
@@ -53,7 +53,7 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
53
53
  ?disabled=${this.disabled}
54
54
  tabindex=${alone || (this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
55
55
  @click=${() => this.select()}
56
- ><slot></slot></button>`;
56
+ ><slot name="start"></slot><slot></slot><slot name="end"></slot></button>`;
57
57
  }
58
58
  };
59
59
  __decorate([
@@ -146,6 +146,11 @@ export declare class HelixHeadlessTreeItem extends HelixHeadlessTreeItem_base {
146
146
  * tree: the item being moved, or the one the drop indicator is beside.
147
147
  */
148
148
  move?: TreeItemMove | undefined;
149
+ /**
150
+ * @internal The item's place among its siblings, counting from one, written
151
+ * by the tree for `aria-posinset`, for the reason `level` gives.
152
+ */
153
+ position?: number | undefined;
149
154
  /**
150
155
  * The link's `rel` value, which describes how the linked page relates to this
151
156
  * one.
@@ -168,6 +173,11 @@ export declare class HelixHeadlessTreeItem extends HelixHeadlessTreeItem_base {
168
173
  * row is not part of the tree React hydrates.
169
174
  */
170
175
  selected: boolean;
176
+ /**
177
+ * @internal How many siblings the item has, itself included, written by the
178
+ * tree for `aria-setsize`, for the reason `level` gives.
179
+ */
180
+ setSize?: number | undefined;
171
181
  /**
172
182
  * Where the link opens. Use it for a row that leaves the site.
173
183
  */
@@ -174,6 +174,8 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
174
174
  const linked = this.href !== '';
175
175
  const level = this.level ?? 1;
176
176
  const depth = this.level ?? nothing;
177
+ const position = this.position ?? nothing;
178
+ const size = this.setSize ?? nothing;
177
179
  const expanded = this.isBranch ? (this.open ? 'true' : 'false') : nothing;
178
180
  const chosen = this.checkable ? undefined : this.selected ? 'true' : 'false';
179
181
  const checked = this.checkable
@@ -193,6 +195,8 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
193
195
  aria-disabled=${linked ? nothing : off}
194
196
  aria-busy=${linked ? nothing : busy}
195
197
  aria-level=${linked ? nothing : depth}
198
+ aria-posinset=${linked ? nothing : position}
199
+ aria-setsize=${linked ? nothing : size}
196
200
  tabindex=${linked ? nothing : tabIndex}
197
201
  >
198
202
  ${linked
@@ -208,6 +212,8 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
208
212
  aria-disabled=${off}
209
213
  aria-busy=${busy}
210
214
  aria-level=${depth}
215
+ aria-posinset=${position}
216
+ aria-setsize=${size}
211
217
  tabindex=${tabIndex}
212
218
  style="--tree-level: ${level}"
213
219
  @click=${this.handleRowClick}
@@ -255,12 +261,18 @@ __decorate([
255
261
  __decorate([
256
262
  property({ attribute: false })
257
263
  ], HelixHeadlessTreeItem.prototype, "move", void 0);
264
+ __decorate([
265
+ property({ attribute: false, type: Number })
266
+ ], HelixHeadlessTreeItem.prototype, "position", void 0);
258
267
  __decorate([
259
268
  property()
260
269
  ], HelixHeadlessTreeItem.prototype, "rel", void 0);
261
270
  __decorate([
262
271
  property({ type: Boolean })
263
272
  ], HelixHeadlessTreeItem.prototype, "selected", void 0);
273
+ __decorate([
274
+ property({ attribute: false, type: Number })
275
+ ], HelixHeadlessTreeItem.prototype, "setSize", void 0);
264
276
  __decorate([
265
277
  property()
266
278
  ], HelixHeadlessTreeItem.prototype, "target", void 0);