@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
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, afterLinkFollowed, customElement, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, typesCharacter, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -28,6 +28,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
28
28
  this.emptyText = 'No matches';
29
29
  this.filterable = false;
30
30
  this.focusable = true;
31
+ this.items = undefined;
31
32
  this.listLabel = 'Options';
32
33
  this.loading = false;
33
34
  this.loadingText = 'Loading';
@@ -43,6 +44,11 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
43
44
  ],
44
45
  offset: () => 4,
45
46
  onDismiss: () => this.close(),
47
+ onPlace: (placement) => {
48
+ const side = sideOf(placement);
49
+ this.toggleState('placed-bottom', side === 'bottom');
50
+ this.toggleState('placed-top', side === 'top');
51
+ },
46
52
  open: () => this.open,
47
53
  place: () => this.position !== 'item-aligned',
48
54
  placement: 'bottom-start',
@@ -124,7 +130,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
124
130
  const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
125
131
  if (step !== 0 && !this.multiple && !(event.altKey || event.ctrlKey || event.metaKey)) {
126
132
  event.preventDefault();
127
- const reachable = listbox?.options.filter((option) => !option.hidden && !option.disabled) ?? [];
133
+ const reachable = this.items
134
+ ? this.entries.filter((option) => !option.disabled)
135
+ : (listbox?.options.filter((option) => !option.hidden && !option.disabled) ?? []);
128
136
  const next = reachable[Math.max(0, reachable.findIndex((one) => one.value === this.value) + step)];
129
137
  if (next) {
130
138
  this.choose(next.value);
@@ -165,16 +173,29 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
165
173
  return this.partNamed('listbox');
166
174
  }
167
175
  get display() {
168
- const words = this.chosenOptions.map((option) => option.text);
176
+ const chosen = new Set(this.value === '' ? [] : this.value.split(','));
177
+ const words = this.entries.filter((option) => chosen.has(option.value)).map((one) => one.text);
169
178
  if (words.length > 0) {
170
179
  return new Intl.ListFormat(wordsLocale(this), { style: 'short', type: 'unit' }).format(words);
171
180
  }
172
- return this.value !== '' && !this.childrenKnown ? nothing : this.placeholder;
181
+ return this.value !== '' && !this.childrenKnown && !this.items ? nothing : this.placeholder;
173
182
  }
174
183
  get chosenOptions() {
184
+ if (this.items) {
185
+ return [];
186
+ }
175
187
  const chosen = new Set(this.value === '' ? [] : this.value.split(','));
176
188
  return optionsAmong(this.knownChildren).filter((option) => chosen.has(option.value));
177
189
  }
190
+ get entries() {
191
+ if (!this.items) {
192
+ return optionsAmong(this.knownChildren);
193
+ }
194
+ if (this.data?.from !== this.items) {
195
+ this.data = { from: this.items, options: dataOptions(this.items) };
196
+ }
197
+ return this.data.options;
198
+ }
178
199
  renderValue() {
179
200
  const chosen = this.chosenOptions;
180
201
  return this.valueContent && chosen.length > 0 ? this.valueContent(chosen) : this.display;
@@ -226,9 +247,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
226
247
  }
227
248
  }
228
249
  reconcileOptions() {
229
- const options = optionsAmong(this.knownChildren);
230
- if (this.defaultValue === '' && !this.hasAttribute('default-value')) {
231
- const declared = options
250
+ const options = this.entries;
251
+ if (!this.items && this.defaultValue === '' && !this.hasAttribute('default-value')) {
252
+ const declared = optionsAmong(this.knownChildren)
232
253
  .filter((option) => option.defaultSelected)
233
254
  .map((option) => option.value);
234
255
  if (declared.length > 0) {
@@ -255,7 +276,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
255
276
  willUpdate(changed) {
256
277
  super.willUpdate(changed);
257
278
  hostAria(this.internals, { label: this.label });
258
- if (this.childrenKnown) {
279
+ if (this.childrenKnown || this.items) {
259
280
  this.reconcileOptions();
260
281
  }
261
282
  }
@@ -298,6 +319,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
298
319
  .value=${this.value}
299
320
  ?multiple=${this.multiple}
300
321
  ?filterable=${this.filterable}
322
+ .items=${this.items}
301
323
  manual
302
324
  aria-label=${this.listName || this.listLabel || nothing}
303
325
  @keydown=${this.handleListKeydown}
@@ -357,7 +379,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
357
379
  disabled: this.isDisabled,
358
380
  multiple: this.multiple,
359
381
  onFill: (value) => this.requestValue(value),
360
- options: optionsAmong(this.knownChildren),
382
+ options: this.entries,
361
383
  value: this.value,
362
384
  })}`;
363
385
  }
@@ -375,6 +397,9 @@ __decorate([
375
397
  __decorate([
376
398
  property({ attribute: false })
377
399
  ], HelixHeadlessSelect.prototype, "focusable", void 0);
400
+ __decorate([
401
+ property({ attribute: false })
402
+ ], HelixHeadlessSelect.prototype, "items", void 0);
378
403
  __decorate([
379
404
  words,
380
405
  property({ attribute: 'list-label' })
@@ -19,7 +19,12 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
19
19
  * does not draw a hundred and one marks by accident.
20
20
  *
21
21
  * A range with two thumbs is two inputs stacked on one track, because there is
22
- * no native two-thumb range. Each thumb keeps its own value.
22
+ * no native two-thumb range. Each thumb keeps its own value, and a named range
23
+ * submits one entry per thumb under its name, the lowest first. A range given
24
+ * more than two values, such as `default-value="20,50,80"`, has a thumb for
25
+ * each, as React Aria's slider takes any number of thumbs: each stops at its
26
+ * neighbors, and the ones between the first and the last are named by
27
+ * `thumb-label`.
23
28
  * `min-steps-between-thumbs` keeps them that many steps apart, and the readout
24
29
  * joins the two as `range-text` writes them.
25
30
  *
@@ -50,6 +55,8 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
50
55
  * | Page Up, Page Down | Moves the thumb by a larger step. |
51
56
  * | Home, End | Moves the thumb to its lowest or highest value. |
52
57
  *
58
+ * Under `readonly` none of these move the thumb, and Tab still reaches it.
59
+ *
53
60
  * Each thumb is an `<input type="range">` with `aria-valuetext` where the value
54
61
  * is formatted or worded. A range's thumbs say they stop at each other with
55
62
  * `aria-valuemin` and `aria-valuemax`.
@@ -63,12 +70,14 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
63
70
  * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
64
71
  * @slot hint - Help under the control, for content richer than the `hint` text.
65
72
  * @slot label - The label, for content richer than the `label` text.
73
+ * @slot thumb - Content drawn on the thumb, such as a glyph: a single slider's thumb, or a range's lower one. It is decoration, hidden from a screen reader.
74
+ * @slot thumb-end - Content drawn on a range's upper thumb.
66
75
  *
67
76
  * @event {Event} change - Fired when the user releases the thumb.
68
77
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the value changes. Cancelable: call `preventDefault()` to keep the current value.
69
78
  * @event {Event} input - Fired after the value changes.
70
79
  *
71
- * @csspart control - The range input.
80
+ * @csspart control - The range input, one per thumb. On a range each carries `data-thumb`, `start`, `middle`, or `end`, and `data-index`, its place from the lowest.
72
81
  * @csspart feedback - The error message under the track.
73
82
  * @csspart field - The row that holds the control and its readout.
74
83
  * @csspart hint - The help text under the track, shown while there is no error.
@@ -77,10 +86,12 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
77
86
  * @csspart marker-label - The value a mark stands at, when `marker-labels` is set.
78
87
  * @csspart markers - The row of marks along the track.
79
88
  * @csspart output - The current value, shown beside the track.
80
- * @csspart tooltip - One thumb's value in a bubble beside it, under `tooltip`. It carries `data-thumb` on a range, `start` or `end`, and `--slider-at`, where its thumb is as a fraction of the track.
89
+ * @csspart thumb - One thumb's own box over the track, which holds the `thumb` or `thumb-end` slot. It carries `data-thumb` on a range, `start`, `middle`, or `end`, and `--slider-at`, where its thumb is as a fraction of the track. Draw your own thumb here and hide the native one, or leave it empty.
90
+ * @csspart thumbs - The layer over the track that holds each `thumb`. It is `hidden` until a thumb slot has content, and then `[part="field"]` carries `data-thumbs`.
91
+ * @csspart tooltip - One thumb's value in a bubble beside it, under `tooltip`. It carries `data-thumb` on a range, `start`, `middle`, or `end`, and `--slider-at`, where its thumb is as a fraction of the track.
81
92
  * @csspart tooltips - The layer over the track that holds the bubbles, under `tooltip`.
82
93
  *
83
- * @cssproperty --slider-at - Where a bubble's thumb is, as a fraction of the track, 0 to 1. Set on each `tooltip` part.
94
+ * @cssproperty --slider-at - Where a bubble's or a thumb box's thumb is, as a fraction of the track, 0 to 1. Set on each `tooltip` and `thumb` part.
84
95
  * @cssproperty --slider-fraction - The value as a fraction of the range, 0 to 1.
85
96
  * @cssproperty --slider-from - Where the filled span starts, as a fraction of the track, 0 to 1: a range's lower thumb, or a single thumb's `origin`.
86
97
  * @cssproperty --slider-marker-at - Where one marker sits, as a percentage of the track.
@@ -185,9 +196,11 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
185
196
  /**
186
197
  * Adds a second thumb, so the value is a pair.
187
198
  *
188
- * The pair is one string, `"10,50"`, which is what a form submits and what
189
- * `MixinControllerValue` already holds. `valueAsNumber` reads the lower of
190
- * the two, since a range has no single number.
199
+ * The pair is one string, `"10,50"`, which is what `value` and
200
+ * `MixinControllerValue` hold. A form receives two entries under the name,
201
+ * `price=10&price=50`, as React Aria's two thumbs send theirs.
202
+ * `valueAsNumber` reads the lower of the two, since a range has no single
203
+ * number.
191
204
  */
192
205
  range: boolean;
193
206
  /**
@@ -195,12 +208,27 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
195
208
  * filled in. Write it in the page's language, such as `{start} bis {end}`.
196
209
  */
197
210
  rangeText: string;
211
+ /**
212
+ * Whether a reader can see and focus the value and cannot change it. A
213
+ * read-only slider still submits its value.
214
+ *
215
+ * React Aria's `isReadOnly`. Each thumb stays a tab stop and says
216
+ * `aria-readonly`. The arrow keys, the page keys, Home, End, and a drag
217
+ * leave the value where it is. A script can still set `value`.
218
+ */
219
+ readOnly: boolean;
198
220
  /**
199
221
  * Shows the chosen value beside the track.
200
222
  */
201
223
  showValue: boolean;
202
224
  /** How far one press of an arrow key moves the thumb. */
203
225
  step: number;
226
+ /**
227
+ * The name of a thumb between a range's first and last, read after the
228
+ * slider's label, with `{index}` filled in: its place, counted from 1. The
229
+ * first is `minimum-label` and the last `maximum-label`.
230
+ */
231
+ thumbLabel: string;
204
232
  /**
205
233
  * Whether each thumb shows its value in a bubble beside it, as Web
206
234
  * Awesome's `with-tooltip`. The styled slider shows the bubbles while the
@@ -258,12 +286,29 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
258
286
  * is what a filter with nothing filtered out looks like.
259
287
  */
260
288
  private get bounds();
289
+ /**
290
+ * A range's values, one per thumb, as numbers in order.
291
+ *
292
+ * A lone number is not a range, so it is not read as one. `willUpdate`
293
+ * seeds and sanitizes a range to two values or more, and anything else that
294
+ * arrives between the two opens fully wide, which is what "nothing filtered
295
+ * out" looks like.
296
+ */
297
+ private get points();
261
298
  /** Where one value sits along the track, as a fraction of it. */
262
299
  private along;
263
300
  private readonly handleInput;
264
301
  /** Relayed, because `change` does not cross a shadow boundary on its own. */
265
302
  private readonly handleChange;
266
- /** Reports the value. */
303
+ /**
304
+ * Keeps a read-only thumb still under the keys that move it. Tab and every
305
+ * other key pass, so focus moves on.
306
+ */
307
+ private readonly handleKeydown;
308
+ /**
309
+ * Reports the value: one entry, or for a named range one entry per thumb.
310
+ * The joined pair is the state the browser restores.
311
+ */
267
312
  private sync;
268
313
  /**
269
314
  * How far along the track the thumb sits, as a fraction.
@@ -325,18 +370,35 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
325
370
  /** What a screen reader says for one thumb, where `value-text` asks. */
326
371
  private spoken;
327
372
  protected updated(changed: PropertyValues): void;
373
+ /** Draws, once a served slider has hydrated, what its children ask for. */
374
+ protected firstUpdated(changed: PropertyValues): void;
328
375
  /**
329
376
  * What a screen reader says for a thumb's value, with `{value}` filled in,
330
377
  * such as `{value} degrees`. Empty announces the number alone.
331
378
  */
332
379
  valueText: string;
380
+ /**
381
+ * Whether a `thumb` or `thumb-end` slot has content, read off the children
382
+ * rather than heard from `slotchange`: a declarative shadow root fires that
383
+ * before anything listens, so a served slider kept its layer hidden.
384
+ */
385
+ private get thumbContent();
386
+ private readonly handleThumbSlot;
387
+ /**
388
+ * A box for each thumb, over the track at the thumb's place, holding the
389
+ * `thumb` slot: a range's upper thumb holds `thumb-end`. Hidden from the
390
+ * accessibility tree, since each thumb is already a range with a name and a
391
+ * value, and `hidden` while no slot has content, so an empty layer takes no
392
+ * room in the row.
393
+ */
394
+ protected renderThumbs(): TemplateResult;
333
395
  /**
334
396
  * A bubble for each thumb, under `tooltip`, saying its value where it is.
335
397
  * After the thumbs, so a stylesheet can reach a bubble from a focused
336
398
  * thumb, and hidden from the accessibility tree for the reason the readout
337
399
  * is: each thumb already reports its value.
338
400
  */
339
- protected renderTooltips(start: number, end: number): TemplateResult | typeof nothing;
401
+ protected renderTooltips(): TemplateResult | typeof nothing;
340
402
  protected render(): TemplateResult;
341
403
  }
342
404
  declare global {
@@ -4,9 +4,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, customElement, formatNumber, formatWords, HelixFormElement, MixinControllerValue, MixinField, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, formatNumber, formatWords, HelixFormElement, MixinControllerValue, MixinField, reportValues, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ const SLIDER_KEYS = new Set([
11
+ 'ArrowDown',
12
+ 'ArrowLeft',
13
+ 'ArrowRight',
14
+ 'ArrowUp',
15
+ 'End',
16
+ 'Home',
17
+ 'PageDown',
18
+ 'PageUp',
19
+ ]);
10
20
  let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue(MixinField(HelixFormElement)) {
11
21
  constructor() {
12
22
  super(...arguments);
@@ -24,21 +34,29 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
24
34
  this.minStepsBetweenThumbs = 0;
25
35
  this.range = false;
26
36
  this.rangeText = '{start} to {end}';
37
+ this.readOnly = false;
27
38
  this.showValue = false;
28
39
  this.step = 1;
40
+ this.thumbLabel = 'thumb {index}';
29
41
  this.tooltip = false;
30
42
  this.handleInput = (event) => {
31
43
  event.stopPropagation();
32
44
  const control = event.target;
45
+ const at = Number(control.dataset.index ?? 0);
46
+ if (this.readOnly) {
47
+ restoreValue(control, this.range ? String(this.points[at]) : this.value);
48
+ return;
49
+ }
33
50
  if (this.range) {
34
- const [start, end] = this.bounds;
51
+ const points = this.points;
35
52
  const moved = Number(control.value);
36
53
  const gap = this.gap;
37
- const pair = control.dataset.thumb === 'start'
38
- ? [Math.min(moved, end - gap), end]
39
- : [start, Math.max(moved, start + gap)];
40
- this.requestValue(pair.join(','));
41
- restoreValue(control, String(control.dataset.thumb === 'start' ? this.bounds[0] : this.bounds[1]));
54
+ const low = at > 0 ? (points[at - 1] ?? this.min) + gap : Number.NEGATIVE_INFINITY;
55
+ const high = at < points.length - 1 ? (points[at + 1] ?? this.max) - gap : Number.POSITIVE_INFINITY;
56
+ this.requestValue(points
57
+ .map((one, index) => (index === at ? Math.min(high, Math.max(low, moved)) : one))
58
+ .join(','));
59
+ restoreValue(control, String(this.points[at]));
42
60
  return;
43
61
  }
44
62
  const next = control.value;
@@ -47,16 +65,30 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
47
65
  };
48
66
  this.handleChange = (event) => {
49
67
  event.stopPropagation();
50
- this.announceChange();
68
+ if (!this.readOnly) {
69
+ this.announceChange();
70
+ }
71
+ };
72
+ this.handleKeydown = (event) => {
73
+ if (this.readOnly && SLIDER_KEYS.has(event.key)) {
74
+ event.preventDefault();
75
+ }
51
76
  };
52
77
  this.valueText = '';
78
+ this.handleThumbSlot = () => {
79
+ this.requestUpdate();
80
+ };
53
81
  }
54
82
  static { this.words = 'helix-slider'; }
55
83
  get reserve() {
56
84
  const widest = Math.max(this.said(this.min).length, this.said(this.max).length);
57
- return this.range
58
- ? widest * 2 + formatWords(this, 'rangeText', { end: '', start: '' }).length
59
- : widest;
85
+ const count = this.points.length;
86
+ if (!this.range) {
87
+ return widest;
88
+ }
89
+ return count > 2
90
+ ? widest * count + 2 * (count - 1) + 4
91
+ : widest * 2 + formatWords(this, 'rangeText', { end: '', start: '' }).length;
60
92
  }
61
93
  get precision() {
62
94
  return String(this.step).split('.')[1]?.length ?? 0;
@@ -83,13 +115,15 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
83
115
  delegatesFocus: true,
84
116
  }; }
85
117
  get bounds() {
86
- const parts = this.value.split(',');
87
- const from = Number.parseFloat(parts[0] ?? '');
88
- const to = Number.parseFloat(parts[1] ?? '');
89
- if (!Number.isFinite(from) || !Number.isFinite(to)) {
118
+ const points = this.points;
119
+ return [points[0] ?? this.min, points.at(-1) ?? this.max];
120
+ }
121
+ get points() {
122
+ const parts = this.value.split(',').map((part) => Number.parseFloat(part));
123
+ if (parts.length < 2 || parts.some((part) => !Number.isFinite(part))) {
90
124
  return [this.min, this.max];
91
125
  }
92
- return [Math.min(from, to), Math.max(from, to)];
126
+ return parts.sort((left, right) => left - right);
93
127
  }
94
128
  along(at) {
95
129
  const span = this.max - this.min;
@@ -99,7 +133,12 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
99
133
  return Math.min(1, Math.max(0, (at - this.min) / span));
100
134
  }
101
135
  sync() {
102
- this.internals.setFormValue(this.value);
136
+ if (this.range && this.name) {
137
+ reportValues(this.internals, this.name, this.points.map(String));
138
+ }
139
+ else {
140
+ this.internals.setFormValue(this.value);
141
+ }
103
142
  this.refreshFeedback();
104
143
  }
105
144
  get fraction() {
@@ -146,15 +185,14 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
146
185
  }
147
186
  sanitize(text) {
148
187
  if (this.range) {
149
- const [from, to] = text.split(',').map((part) => Number.parseFloat(part));
150
- if (from === undefined ||
151
- to === undefined ||
152
- !Number.isFinite(from) ||
153
- !Number.isFinite(to)) {
188
+ const values = text.split(',').map((part) => Number.parseFloat(part));
189
+ if (values.length < 2 || values.some((one) => !Number.isFinite(one))) {
154
190
  return `${this.min},${this.max}`;
155
191
  }
156
- const ends = [this.settled(from), this.settled(to)].sort((a, b) => a - b);
157
- return ends.join(',');
192
+ return values
193
+ .map((one) => this.settled(one))
194
+ .sort((a, b) => a - b)
195
+ .join(',');
158
196
  }
159
197
  const parsed = Number.parseFloat(text);
160
198
  const at = Number.isFinite(parsed) ? parsed : this.min + (this.max - this.min) / 2;
@@ -188,7 +226,31 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
188
226
  super.updated(changed);
189
227
  this.sync();
190
228
  }
191
- renderTooltips(start, end) {
229
+ firstUpdated(changed) {
230
+ super.firstUpdated(changed);
231
+ this.finishHydration();
232
+ }
233
+ get thumbContent() {
234
+ return this.knownChildren.some((child) => child.slot === 'thumb' || child.slot === 'thumb-end');
235
+ }
236
+ renderThumbs() {
237
+ const slot = (end) => end
238
+ ? html `<slot name="thumb-end" @slotchange=${this.handleThumbSlot}></slot>`
239
+ : html `<slot name="thumb" @slotchange=${this.handleThumbSlot}></slot>`;
240
+ const box = (fraction, end, thumb) => html `<span part="thumb" data-thumb=${thumb ?? nothing} style="--slider-at: ${fraction}"
241
+ >${slot(end)}</span
242
+ >`;
243
+ return html `<div part="thumbs" aria-hidden="true" ?hidden=${!this.thumbContent}>
244
+ ${this.range
245
+ ? this.points.map((at, index, all) => index === 0
246
+ ? box(this.along(at), false, 'start')
247
+ : index === all.length - 1
248
+ ? box(this.along(at), true, 'end')
249
+ : html `<span part="thumb" data-thumb="middle" style="--slider-at: ${this.along(at)}"></span>`)
250
+ : box(this.fraction, false)}
251
+ </div>`;
252
+ }
253
+ renderTooltips() {
192
254
  if (!this.tooltip) {
193
255
  return nothing;
194
256
  }
@@ -197,49 +259,60 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
197
259
  >`;
198
260
  return html `<div part="tooltips" aria-hidden="true">
199
261
  ${this.range
200
- ? [
201
- bubble(this.along(start), this.said(start), 'start'),
202
- bubble(this.along(end), this.said(end), 'end'),
203
- ]
262
+ ? this.points.map((at, index, all) => bubble(this.along(at), this.said(at), index === 0 ? 'start' : index === all.length - 1 ? 'end' : 'middle'))
204
263
  : bubble(this.fraction, this.said(this.value))}
205
264
  </div>`;
206
265
  }
207
266
  render() {
208
267
  const [start, end] = this.bounds;
209
- const thumb = (which, at) => html `<input
268
+ const ids = this.points.map((_, index, all) => index === 0 ? 'control' : index === all.length - 1 ? 'control-end' : `control-${index}`);
269
+ const points = this.points;
270
+ const last = points.length - 1;
271
+ const thumb = (at, index) => {
272
+ const which = index === 0 ? 'start' : index === last ? 'end' : 'middle';
273
+ const named = which === 'middle' ? `thumb-name-${index}` : `${which}-name`;
274
+ const before = points[index - 1];
275
+ const after = points[index + 1];
276
+ return html `<input
210
277
  part="control"
211
- id=${which === 'start' ? 'control' : 'control-end'}
278
+ id=${ids[index] ?? nothing}
212
279
  data-thumb=${which}
280
+ data-index=${index}
213
281
  type="range"
214
282
  style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}${this.axis}"
215
283
  min=${this.min}
216
284
  max=${this.max}
217
285
  step=${this.step}
218
286
  .value=${String(at)}
219
- aria-labelledby=${this.labeled ? `${this.labelId} ${which}-name` : `${which}-name`}
220
- aria-valuemin=${which === 'end' ? start + this.gap : nothing}
221
- aria-valuemax=${which === 'start' ? end - this.gap : nothing}
287
+ aria-labelledby=${this.labeled ? `${this.labelId} ${named}` : named}
288
+ aria-valuemin=${before === undefined ? nothing : before + this.gap}
289
+ aria-valuemax=${after === undefined ? nothing : after - this.gap}
222
290
  aria-valuetext=${this.spoken(at)}
223
291
  aria-describedby=${this.describedBy ?? nothing}
224
292
  aria-invalid=${this.invalidity ?? nothing}
293
+ aria-readonly=${this.readOnly ? 'true' : nothing}
225
294
  ?disabled=${this.isDisabled}
226
295
  @input=${this.handleInput}
227
296
  @change=${this.handleChange}
297
+ @keydown=${this.handleKeydown}
228
298
  />`;
299
+ };
229
300
  return html `${this.renderLabel()}
230
301
  ${this.range
231
302
  ? html `<span id="start-name" hidden>${this.minimumLabel}</span
232
- ><span id="end-name" hidden>${this.maximumLabel}</span>`
303
+ >${this.points.slice(1, -1).map((_, index) => html `<span id=${`thumb-name-${index + 1}`} hidden
304
+ >${formatWords(this, 'thumbLabel', { index: index + 2 })}</span
305
+ >`)}<span id="end-name" hidden>${this.maximumLabel}</span>`
233
306
  : nothing}
234
307
  <div
235
308
  part="field"
236
309
  data-markers=${this.markerValues.length > 0 ? '' : nothing}
237
310
  data-range=${this.range ? '' : nothing}
311
+ data-thumbs=${this.thumbContent ? '' : nothing}
238
312
  data-tooltip=${this.tooltip ? '' : nothing}
239
313
  style="--slider-fraction: ${this.fraction}; --slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}"
240
314
  >
241
- ${this.range ? thumb('start', start) : nothing}
242
- ${this.range ? thumb('end', end) : nothing}
315
+ ${this.range ? this.points.map((at, index) => thumb(at, index)) : nothing}
243
316
  ${this.range
244
317
  ? nothing
245
318
  : html `<input
@@ -256,9 +329,11 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
256
329
  aria-valuetext=${this.spoken(this.value)}
257
330
  aria-describedby=${this.describedBy ?? nothing}
258
331
  aria-invalid=${this.invalidity ?? nothing}
332
+ aria-readonly=${this.readOnly ? 'true' : nothing}
259
333
  ?disabled=${this.isDisabled}
260
334
  @input=${this.handleInput}
261
335
  @change=${this.handleChange}
336
+ @keydown=${this.handleKeydown}
262
337
  />`}
263
338
  ${this.markers > 1
264
339
  ? html `<div part="markers" aria-hidden="true">
@@ -271,16 +346,21 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
271
346
  >`)}
272
347
  </div>`
273
348
  : nothing}
274
- ${this.renderTooltips(start, end)}
349
+ ${this.renderThumbs()} ${this.renderTooltips()}
275
350
  ${this.showValue
276
351
  ? html `<output
277
352
  part="output"
278
- for=${this.range ? 'control control-end' : 'control'}
353
+ for=${this.range ? ids.join(' ') : 'control'}
279
354
  aria-hidden="true"
280
355
  style=${`--_slider-output-reserve: ${this.reserve}ch`}
281
- >${this.range
282
- ? formatWords(this, 'rangeText', { end: this.said(end), start: this.said(start) })
283
- : this.said(this.value)}</output
356
+ >${!this.range
357
+ ? this.said(this.value)
358
+ : this.points.length > 2
359
+ ? new Intl.ListFormat(wordsLocale(this), { type: 'conjunction' }).format(this.points.map((at) => this.said(at)))
360
+ : formatWords(this, 'rangeText', {
361
+ end: this.said(end),
362
+ start: this.said(start),
363
+ })}</output
284
364
  >`
285
365
  : nothing}
286
366
  </div>
@@ -332,12 +412,19 @@ __decorate([
332
412
  words,
333
413
  property({ attribute: 'range-text' })
334
414
  ], HelixHeadlessSlider.prototype, "rangeText", void 0);
415
+ __decorate([
416
+ property({ attribute: 'readonly', reflect: true, type: Boolean })
417
+ ], HelixHeadlessSlider.prototype, "readOnly", void 0);
335
418
  __decorate([
336
419
  property({ attribute: 'show-value', reflect: true, type: Boolean })
337
420
  ], HelixHeadlessSlider.prototype, "showValue", void 0);
338
421
  __decorate([
339
422
  property({ type: Number })
340
423
  ], HelixHeadlessSlider.prototype, "step", void 0);
424
+ __decorate([
425
+ words,
426
+ property({ attribute: 'thumb-label' })
427
+ ], HelixHeadlessSlider.prototype, "thumbLabel", void 0);
341
428
  __decorate([
342
429
  property({ type: Boolean })
343
430
  ], HelixHeadlessSlider.prototype, "tooltip", void 0);
@@ -19,6 +19,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
19
19
  * Put a `Kbd` in the `shortcut` slot to draw it as keys instead; the dial
20
20
  * imports no `Kbd` of its own. Showing a shortcut does not bind it.
21
21
  *
22
+ * `href` makes the action a link, as Menu's rows take one: the `item` part is
23
+ * then an `<a>` carrying `menuitem`, so the browser follows it, opens it in a
24
+ * new tab on a middle press, and offers its own menu for it. Choosing it still
25
+ * sends `helix-select` and closes the dial. A disabled action draws no link.
26
+ *
22
27
  * @since 0.1.0
23
28
  * @status stable
24
29
  * @category actions
@@ -32,7 +37,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
32
37
  * @event {HelixSelectEvent} helix-select - The action was chosen. Cancelable: cancel it to keep the dial open.
33
38
  *
34
39
  * @csspart icon - The box that holds the icon.
35
- * @csspart item - The control for the action.
40
+ * @csspart item - The control for the action, which is an `<a>` where `href` is set.
36
41
  * @csspart label - The action's name, beside the icon.
37
42
  * @csspart shortcut - The box after the name that holds the shortcut, shown while `shortcut` is set or something is in the `shortcut` slot.
38
43
  */
@@ -41,12 +46,30 @@ export declare class HelixHeadlessSpeedDialItem extends HelixElement {
41
46
  * Whether the action is disabled, so it cannot be chosen.
42
47
  */
43
48
  disabled: boolean;
49
+ /**
50
+ * Whether the link downloads the file when the action is chosen. Only
51
+ * meaningful beside an `href`, and only for a file served from the page's
52
+ * origin.
53
+ */
54
+ download: boolean;
55
+ /**
56
+ * The URL the action links to. Setting it draws the action as a link.
57
+ */
58
+ href: string;
59
+ /**
60
+ * The link's `rel`. Set it whenever you set `target="_blank"`.
61
+ */
62
+ rel: string;
44
63
  /**
45
64
  * The action's keyboard shortcut as a chord, such as `mod+s`, carried as
46
65
  * `aria-keyshortcuts` and drawn after the name. `mod` is Command on an Apple
47
66
  * keyboard and Control elsewhere. Showing a shortcut does not bind it.
48
67
  */
49
68
  shortcut: string;
69
+ /**
70
+ * Where the link opens, such as `_blank` for a new tab.
71
+ */
72
+ target: string;
50
73
  /**
51
74
  * The action's value, sent in `helix-select` when the action is chosen.
52
75
  */
@@ -86,6 +109,11 @@ export declare class HelixHeadlessSpeedDialItem extends HelixElement {
86
109
  private readonly readShortcut;
87
110
  private choose;
88
111
  private readonly handleClick;
112
+ /**
113
+ * Space presses a linked action, which an anchor ignores, and the click that
114
+ * comes back both announces and follows the link. Enter is the anchor's own.
115
+ */
116
+ private readonly handleKeydown;
89
117
  protected render(): TemplateResult;
90
118
  /**
91
119
  * The `shortcut` slot, holding the shortcut drawn after the name as Kbd