@fusion.dev/helix-headless 0.169.0 → 0.171.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 (41) hide show
  1. package/custom-elements.json +663 -95
  2. package/dist/chat/composer/chat-composer.d.ts +2 -1
  3. package/dist/chat/composer/chat-composer.js +7 -1
  4. package/dist/chat/root/chat-root.d.ts +36 -6
  5. package/dist/chat/root/chat-root.js +53 -5
  6. package/dist/code-block/root/code-block-root.d.ts +20 -2
  7. package/dist/code-block/root/code-block-root.js +29 -8
  8. package/dist/color-picker/root/color-picker-root.d.ts +50 -7
  9. package/dist/color-picker/root/color-picker-root.js +62 -1
  10. package/dist/combobox/root/combobox-root.d.ts +16 -6
  11. package/dist/combobox/root/combobox-root.js +9 -2
  12. package/dist/command/root/command-root.d.ts +16 -1
  13. package/dist/command/root/command-root.js +8 -0
  14. package/dist/data-table/root/data-table-root.d.ts +40 -5
  15. package/dist/data-table/root/data-table-root.js +35 -7
  16. package/dist/filter-input/chip/filter-chip.d.ts +13 -3
  17. package/dist/filter-input/chip/filter-chip.js +5 -0
  18. package/dist/filter-input/menu/filter-menu.js +0 -1
  19. package/dist/filter-input/root/filter-input-root.d.ts +71 -4
  20. package/dist/filter-input/root/filter-input-root.js +87 -2
  21. package/dist/format-date/root/format-date-root.d.ts +3 -1
  22. package/dist/input/root/input-root.d.ts +6 -0
  23. package/dist/input/root/input-root.js +13 -0
  24. package/dist/listbox/root/listbox-root.d.ts +6 -0
  25. package/dist/listbox/root/listbox-root.js +6 -1
  26. package/dist/number-input/root/number-input-root.d.ts +16 -6
  27. package/dist/number-input/root/number-input-root.js +15 -4
  28. package/dist/select/root/select-root.js +2 -0
  29. package/dist/steps/root/steps-root.d.ts +32 -8
  30. package/dist/steps/root/steps-root.js +70 -9
  31. package/dist/steps/step/step.d.ts +9 -1
  32. package/dist/steps/step/step.js +5 -0
  33. package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
  34. package/dist/video-playlist/root/video-playlist-root.js +14 -3
  35. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  36. package/dist/virtual-list/root/virtual-list-root.js +7 -2
  37. package/lint.json +1 -1
  38. package/package.json +5 -5
  39. package/types/jsx.d.ts +510 -126
  40. package/types/vue.d.ts +109 -26
  41. package/types/words.d.ts +12 -2
@@ -21,9 +21,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
21
21
  target: () => this.partNamed('control'),
22
22
  });
23
23
  this.announcesInput = true;
24
+ this.chips = false;
24
25
  this.clearable = false;
25
26
  this.clearLabel = 'Clear';
26
27
  this.filterSuggestions = false;
28
+ this.loading = false;
29
+ this.loadingText = 'Loading';
30
+ this.noResultsText = '';
27
31
  this.placeholder = '';
28
32
  this.suggestionsLabel = '{count, plural, one {# suggestion} other {# suggestions}}';
29
33
  this.status = new StatusController(this);
@@ -59,6 +63,17 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
59
63
  event.stopPropagation();
60
64
  this.announceChange();
61
65
  };
66
+ this.handleFacet = (event) => {
67
+ if (!this.chips) {
68
+ return;
69
+ }
70
+ const facet = event.detail;
71
+ queueMicrotask(() => {
72
+ if (!event.defaultPrevented) {
73
+ this.drawChip(facet);
74
+ }
75
+ });
76
+ };
62
77
  this.handleDismiss = (event) => {
63
78
  const chip = event.composedPath()[0];
64
79
  if (!(chip instanceof HTMLElement) || chip.slot !== 'tags') {
@@ -161,6 +176,20 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
161
176
  };
162
177
  this.handleTokens = () => {
163
178
  this.over = this.hideOverflow(this.tokens);
179
+ for (const token of this.tokenElements()) {
180
+ const quiet = () => {
181
+ if ('dismissTabStop' in token) {
182
+ ;
183
+ token.dismissTabStop = false;
184
+ }
185
+ };
186
+ if (token.localName.includes('-') && !customElements.get(token.localName)) {
187
+ void customElements.whenDefined(token.localName).then(quiet);
188
+ }
189
+ else {
190
+ quiet();
191
+ }
192
+ }
164
193
  };
165
194
  }
166
195
  static { this.words = 'helix-filter-input'; }
@@ -181,14 +210,22 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
181
210
  }
182
211
  }
183
212
  sayCount() {
184
- const count = this.open && this.suggesting ? this.suggestions.length : -1;
213
+ const count = this.open && this.suggesting ? (this.loading ? -2 : this.suggestions.length) : -1;
185
214
  if (count === this.saidCount) {
186
215
  return;
187
216
  }
188
217
  this.saidCount = count;
218
+ if (count === -2) {
219
+ this.status.sayNow(this.loadingText);
220
+ return;
221
+ }
189
222
  if (count < 0) {
190
223
  return;
191
224
  }
225
+ if (count === 0 && this.noResultsText !== '') {
226
+ this.status.sayNow(this.noResultsText);
227
+ return;
228
+ }
192
229
  this.status.sayNow(this.suggestionLabel
193
230
  ? countLabel(count, this.suggestionLabel, this.suggestionsLabel, wordsLocale(this))
194
231
  : formatWords(this, 'suggestionsLabel', { count }));
@@ -259,6 +296,25 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
259
296
  ? menu.apply(facet.field, facet.values, facet.operator)
260
297
  : this.dispatchEvent(new HelixFacetEvent(facet));
261
298
  }
299
+ drawChip(facet) {
300
+ const tag = `${this.constructor.tier}filter-chip`;
301
+ const held = [...this.children].find((child) => child.localName === tag && child.getAttribute('name') === facet.field);
302
+ if (facet.values.length === 0 && facet.arity !== 'none') {
303
+ held?.remove();
304
+ return;
305
+ }
306
+ const chip = held ?? document.createElement(tag);
307
+ chip.slot = 'tags';
308
+ chip.dismissible = true;
309
+ chip.label = facet.label;
310
+ chip.setAttribute('name', facet.field);
311
+ chip.operator = facet.operator;
312
+ chip.operatorLabel = facet.operatorLabel;
313
+ chip.textContent = facet.values.join(', ');
314
+ if (!held) {
315
+ this.append(chip);
316
+ }
317
+ }
262
318
  readFilters() {
263
319
  const fields = this.fields;
264
320
  if (fields.length === 0) {
@@ -310,9 +366,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
310
366
  refreshSuggesting() {
311
367
  this.narrow();
312
368
  const options = this.suggestions;
313
- this.suggesting = options.length > 0;
369
+ this.suggesting = options.length > 0 || this.loading || this.unmatched;
314
370
  this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
315
371
  }
372
+ get unmatched() {
373
+ const said = this.noResultsText !== '' || this.knownChildren.some((child) => child.slot === 'no-results');
374
+ return said && !this.loading && this.options.length > 0 && this.suggestions.length === 0;
375
+ }
316
376
  tokenElements() {
317
377
  return (this.tokens?.assignedElements({ flatten: true }) ?? []);
318
378
  }
@@ -328,10 +388,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
328
388
  connectedCallback() {
329
389
  super.connectedCallback();
330
390
  this.addEventListener('helix-dismiss', this.handleDismiss);
391
+ this.addEventListener(HelixFacetEvent.eventName, this.handleFacet);
331
392
  }
332
393
  disconnectedCallback() {
333
394
  super.disconnectedCallback();
334
395
  this.removeEventListener('helix-dismiss', this.handleDismiss);
396
+ this.removeEventListener(HelixFacetEvent.eventName, this.handleFacet);
335
397
  }
336
398
  willUpdate(changed) {
337
399
  super.willUpdate(changed);
@@ -348,6 +410,8 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
348
410
  }
349
411
  this.handleTokens();
350
412
  this.refreshSuggesting();
413
+ this.toggleState('loading', this.loading);
414
+ this.toggleState('no-results', this.unmatched);
351
415
  }
352
416
  firstUpdated(changed) {
353
417
  super.firstUpdated(changed);
@@ -443,11 +507,22 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
443
507
  ?hidden=${!this.open}
444
508
  >
445
509
  <slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
510
+ <!-- Hidden from the tree, which a listbox keeps for its options: the
511
+ live region says these. -->
512
+ <div part="loading" aria-hidden="true" ?hidden=${!this.loading}>
513
+ <slot name="loading">${this.loadingText}</slot>
514
+ </div>
515
+ <div part="empty" aria-hidden="true" ?hidden=${!this.unmatched}>
516
+ <slot name="no-results">${this.noResultsText}</slot>
517
+ </div>
446
518
  </div>
447
519
  ${drawStatus(this.status)}
448
520
  ${this.renderHint()}${this.renderFeedback()}`;
449
521
  }
450
522
  };
523
+ __decorate([
524
+ property({ type: Boolean })
525
+ ], HelixHeadlessFilterInput.prototype, "chips", void 0);
451
526
  __decorate([
452
527
  property({ type: Boolean })
453
528
  ], HelixHeadlessFilterInput.prototype, "clearable", void 0);
@@ -458,6 +533,16 @@ __decorate([
458
533
  __decorate([
459
534
  property({ attribute: 'filter-suggestions', type: Boolean })
460
535
  ], HelixHeadlessFilterInput.prototype, "filterSuggestions", void 0);
536
+ __decorate([
537
+ property({ reflect: true, type: Boolean })
538
+ ], HelixHeadlessFilterInput.prototype, "loading", void 0);
539
+ __decorate([
540
+ words,
541
+ property({ attribute: 'loading-text' })
542
+ ], HelixHeadlessFilterInput.prototype, "loadingText", void 0);
543
+ __decorate([
544
+ property({ attribute: 'no-results-text' })
545
+ ], HelixHeadlessFilterInput.prototype, "noResultsText", void 0);
461
546
  __decorate([
462
547
  property()
463
548
  ], HelixHeadlessFilterInput.prototype, "placeholder", void 0);
@@ -15,7 +15,9 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
15
15
  * **`relative` shows the same reading in words**, such as "3 hours ago" at
16
16
  * `long`, "3 hr. ago" at `short` and "3h ago" at `narrow`, the styles
17
17
  * `sl-relative-time` offers. `numeric="always"` says "1 day ago" where the
18
- * default says "yesterday".
18
+ * default says "yesterday". Both readings round the count, as
19
+ * `sl-relative-time` does: 100 seconds ago is "2 minutes ago", and 59.6
20
+ * minutes is "1 hour".
19
21
  *
20
22
  * **`date-style` and `time-style` take each locale's own pattern**, such as
21
23
  * "Tuesday, August 4, 2026" at `full`, and stand in for the separate fields,
@@ -209,6 +209,12 @@ export declare class HelixHeadlessInput extends HelixHeadlessInput_base {
209
209
  /** Where the selection starts, as an offset into the value. */
210
210
  get selectionStart(): number;
211
211
  set selectionStart(at: number);
212
+ /**
213
+ * Replaces a range of the text, as the native method does: the selection
214
+ * where `start` and `end` are left out, and `selectMode` says what is
215
+ * selected after. It sends no event, as the native method sends none.
216
+ */
217
+ setRangeText(replacement: string, start?: number, end?: number, selectMode?: SelectionMode): void;
212
218
  /**
213
219
  * Selects the range from `start` to `end` in one call, which setting
214
220
  * `selectionStart` and `selectionEnd` one at a time cannot always do.
@@ -97,6 +97,19 @@ let HelixHeadlessInput = class HelixHeadlessInput extends MixinControllerValue(M
97
97
  control.selectionStart = at;
98
98
  }
99
99
  }
100
+ setRangeText(replacement, start, end, selectMode) {
101
+ const control = this.control;
102
+ if (!control) {
103
+ return;
104
+ }
105
+ if (start === undefined || end === undefined) {
106
+ control.setRangeText(replacement);
107
+ }
108
+ else {
109
+ control.setRangeText(replacement, start, end, selectMode);
110
+ }
111
+ this.value = control.value;
112
+ }
100
113
  setSelectionRange(start, end, direction) {
101
114
  this.control?.setSelectionRange(start, end, direction);
102
115
  }
@@ -154,6 +154,12 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
154
154
  emptyText: string;
155
155
  /** What the list is narrowed to, whoever set it. */
156
156
  private filterState;
157
+ /**
158
+ * @internal Set by `Command` under `home-end="list"`: whether Home and End
159
+ * in the filter field move the highlight, without Shift, rather than the
160
+ * caret. A property, as coordination between elements.
161
+ */
162
+ fieldHomeEnd: boolean;
157
163
  /**
158
164
  * The text the list is filtered by, if any.
159
165
  *
@@ -23,6 +23,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
23
23
  this.defaultFilter = '';
24
24
  this.emptyText = 'No matches';
25
25
  this.filterState = undefined;
26
+ this.fieldHomeEnd = false;
26
27
  this.filterable = false;
27
28
  this.filterDelay = 0;
28
29
  this.filterLabel = 'Filter';
@@ -127,7 +128,8 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
127
128
  (event.key === 'Home' || event.key === 'End');
128
129
  const grid = this.layout === 'grid' && !this.data.records;
129
130
  const along = grid && !typing ? inlineStep(this, event.key) : 0;
130
- if (!fieldRange && !along && !(typing ? FIELD_MOVES : MOVES).has(event.key)) {
131
+ const fieldMoves = this.fieldHomeEnd && !event.shiftKey ? MOVES : FIELD_MOVES;
132
+ if (!fieldRange && !along && !(typing ? fieldMoves : MOVES).has(event.key)) {
131
133
  return;
132
134
  }
133
135
  event.preventDefault();
@@ -673,6 +675,9 @@ __decorate([
673
675
  words,
674
676
  property({ attribute: 'empty-text' })
675
677
  ], HelixHeadlessListbox.prototype, "emptyText", void 0);
678
+ __decorate([
679
+ property({ attribute: false })
680
+ ], HelixHeadlessListbox.prototype, "fieldHomeEnd", void 0);
676
681
  __decorate([
677
682
  property()
678
683
  ], HelixHeadlessListbox.prototype, "filter", null);
@@ -124,7 +124,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
124
124
  */
125
125
  badInputMessage: string;
126
126
  /**
127
- * The name of the step down button, read by screen readers.
127
+ * The name of the step down button, read by screen readers. The field's
128
+ * label follows it, so a reader hears "Decrease Quantity", as React Aria
129
+ * names its steppers.
128
130
  */
129
131
  decrementLabel: string;
130
132
  /**
@@ -138,7 +140,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
138
140
  */
139
141
  formatOptions?: Intl.NumberFormatOptions;
140
142
  /**
141
- * The name of the step up button, read by screen readers.
143
+ * The name of the step up button, read by screen readers. The field's
144
+ * label follows it, so a reader hears "Increase Quantity", as React Aria
145
+ * names its steppers.
142
146
  */
143
147
  incrementLabel: string;
144
148
  /**
@@ -321,16 +325,22 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
321
325
  * refused step, or one pinned at a bound, finished nothing.
322
326
  */
323
327
  private land;
324
- /** Steps the value up, by `largeStep` steps when `large` is set. */
325
- stepUp(large?: boolean): void;
326
328
  /**
327
- * Steps the value down, by `largeStep` steps when `large` is set.
329
+ * Steps the value up by `step`, `count` times, as the native method and
330
+ * `Input.stepUp` do. `true` takes `largeStep` steps, as `PageUp` does.
331
+ */
332
+ stepUp(count?: number | boolean): void;
333
+ /**
334
+ * Steps the value down by `step`, `count` times. `true` takes `largeStep`
335
+ * steps, as `PageDown` does.
328
336
  *
329
337
  * The same path a press of the down arrow takes, so a consumer driving this
330
338
  * from their own control gets the clamping and the announcement rather than
331
339
  * having to repeat both.
332
340
  */
333
- stepDown(large?: boolean): void;
341
+ stepDown(count?: number | boolean): void;
342
+ /** How many steps a `stepUp` argument asks for: a count, or `largeStep` for `true`. */
343
+ private stepsOf;
334
344
  /** The timer of a held stepper button. */
335
345
  private repeat;
336
346
  /** Whether a pointer press stepped already, so the click that ends it does not step again. */
@@ -324,11 +324,17 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
324
324
  this.announceChange();
325
325
  }
326
326
  }
327
- stepUp(large = false) {
328
- this.nudge(large ? (this.largeStep ?? 10) : 1);
327
+ stepUp(count = 1) {
328
+ this.nudge(this.stepsOf(count));
329
329
  }
330
- stepDown(large = false) {
331
- this.nudge(large ? -(this.largeStep ?? 10) : -1);
330
+ stepDown(count = 1) {
331
+ this.nudge(-this.stepsOf(count));
332
+ }
333
+ stepsOf(count) {
334
+ if (typeof count === 'boolean') {
335
+ return count ? (this.largeStep ?? 10) : 1;
336
+ }
337
+ return Number.isFinite(count) ? Math.trunc(count) : 1;
332
338
  }
333
339
  disconnectedCallback() {
334
340
  super.disconnectedCallback();
@@ -342,6 +348,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
342
348
  return html `<label
343
349
  part="label"
344
350
  for=${this.controlId}
351
+ id=${this.labelId}
345
352
  ?data-hidden=${hidden !== null}
346
353
  @pointercancel=${this.handleScrubEnd}
347
354
  @pointerdown=${this.handleScrubDown}
@@ -455,8 +462,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
455
462
  <div part="field" role="group" aria-labelledby=${this.labeled ? this.labelId : nothing}>
456
463
  <button
457
464
  part="decrement"
465
+ id="decrement"
458
466
  type="button"
459
467
  aria-label=${this.decrementLabel}
468
+ aria-labelledby=${this.labeled ? `decrement ${this.labelId}` : nothing}
460
469
  tabindex="-1"
461
470
  ?disabled=${this.isDisabled || this.readOnly || atMin}
462
471
  @click=${this.handleStepperClick}
@@ -511,8 +520,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
511
520
  <slot name="end"></slot>
512
521
  <button
513
522
  part="increment"
523
+ id="increment"
514
524
  type="button"
515
525
  aria-label=${this.incrementLabel}
526
+ aria-labelledby=${this.labeled ? `increment ${this.labelId}` : nothing}
516
527
  tabindex="-1"
517
528
  ?disabled=${this.isDisabled || this.readOnly || atMax}
518
529
  @click=${this.handleStepperClick}
@@ -310,6 +310,8 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
310
310
  }
311
311
  else {
312
312
  this.stopAligning();
313
+ this.toggleState('placed-bottom', false);
314
+ this.toggleState('placed-top', false);
313
315
  this.listbox?.requestFilter('');
314
316
  }
315
317
  }
@@ -1,4 +1,4 @@
1
- import { HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
1
+ import { CommandController, HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { HelixHeadlessStep } from '../step/step.js';
4
4
  declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & typeof HelixElement;
@@ -14,12 +14,21 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
14
14
  * sequence is correct in the server's markup, where the root cannot count its
15
15
  * slotted children.
16
16
  *
17
+ * `active` names a step by its `value`. A step with no `value` is named by its
18
+ * place, counted from 1, so a sequence written without values moves as one
19
+ * written with them, and `helix-activate` says `1`, `2` and so on. Give every
20
+ * step a value or none, because a value of `2` and the second place are one
21
+ * name.
22
+ *
17
23
  * `linear` keeps a reader from passing a stage they have not finished, as Ark
18
24
  * UI's linear steps do: a stage ahead is out of reach, and its button disabled,
19
25
  * until every stage before it is `complete` or `optional`. Going back is always
20
26
  * allowed. `next()` and `previous()` move one stage either way, under the
21
27
  * same rules, for your own buttons. `reset()` goes back to the first stage and
22
- * clears every stage's `complete`, as Ark UI's `resetStep` does.
28
+ * clears every stage's `complete`, as Ark UI's `resetStep` does. A button
29
+ * anywhere on the page calls them with no script, as Ark UI's previous and
30
+ * next triggers do: give it `commandfor` with the sequence's `id`, and
31
+ * `command="--previous"`, `"--next"` or `"--reset"`.
23
32
  *
24
33
  * `completion="passed"` marks a stage `complete` once a reader moves past it,
25
34
  * as Ark UI counts a stage before the current one as completed. Going back
@@ -46,11 +55,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
46
55
  * button is described by its `complete-label`.
47
56
  *
48
57
  * **Keyboard.** Each stage's name is a button and its own tab stop, and Enter
49
- * or Space goes to the stage. Set `arrows` and the arrow keys move focus
50
- * between the names as well, as Ark UI's steps do: Left and Right along a row,
51
- * Up and Down a column, Home and End to the first and the last. The arrows
52
- * pass over a disabled stage and one a linear sequence holds out of reach,
53
- * and they move focus without going to the stage.
58
+ * or Space goes to the stage. Set `arrows` and the names are one tab stop, the
59
+ * current stage's, and the arrow keys move focus between them: Left and Right
60
+ * along a row, Up and Down a column, Home and End to the first and the last.
61
+ * The stop follows focus, so Tab leaves the sequence and Shift+Tab comes back
62
+ * to the name the reader left. The arrows pass over a disabled stage and one a
63
+ * linear sequence holds out of reach, and they move focus without going to the
64
+ * stage.
54
65
  *
55
66
  * @since 0.1.0
56
67
  * @status stable
@@ -78,7 +89,8 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
78
89
  static words: string;
79
90
  /**
80
91
  * Whether the arrow keys move focus between the stages' names, along the
81
- * way the steps run. Off by default, so the arrows keep scrolling the page.
92
+ * way the steps run, and the names are one tab stop. Off by default, so the
93
+ * arrows keep scrolling the page.
82
94
  */
83
95
  arrows: boolean;
84
96
  /**
@@ -148,6 +160,7 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
148
160
  */
149
161
  variant?: 'numeric';
150
162
  protected readonly keys: KeyboardNavigationController;
163
+ protected readonly commands: CommandController;
151
164
  /** The steps, in the order the consumer wrote them. */
152
165
  private get steps();
153
166
  /**
@@ -158,6 +171,8 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
158
171
  * calls to `next()` in one task then move two stages.
159
172
  */
160
173
  private get at();
174
+ /** The step `active` names, by `value` or by place, or the first enabled one. */
175
+ private named;
161
176
  /**
162
177
  * Requests a new current stage, and returns whether it moved.
163
178
  *
@@ -207,6 +222,15 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
207
222
  */
208
223
  private sync;
209
224
  private readonly handleSelection;
225
+ /** The step whose name holds the sequence's one tab stop under `arrows`, once focus has chosen it. */
226
+ private holder;
227
+ /**
228
+ * Gives the one tab stop to a name, under `arrows`: the one focus last left,
229
+ * else the current stage's, else the first a reader can go to. Without
230
+ * `arrows` every name is its own stop. Client only.
231
+ */
232
+ private rove;
233
+ private readonly handleFocus;
210
234
  private readonly handleSlotChange;
211
235
  connectedCallback(): void;
212
236
  disconnectedCallback(): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, focusItemOf, formatWords, HelixElement, hostAria, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { CommandController, customElement, focusItemOf, formatWords, HelixElement, hostAria, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { canReach, progressOf } from '../reach.js';
@@ -19,6 +19,7 @@ const navigable = (step) => {
19
19
  },
20
20
  });
21
21
  };
22
+ const keyOf = (step, index) => step.value || String(index + 1);
22
23
  let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
23
24
  constructor() {
24
25
  super(...arguments);
@@ -33,6 +34,13 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
33
34
  selects: () => false,
34
35
  wrap: false,
35
36
  });
37
+ this.commands = new CommandController(this, {
38
+ commands: {
39
+ '--next': () => this.next(),
40
+ '--previous': () => this.previous(),
41
+ '--reset': () => this.reset(),
42
+ },
43
+ });
36
44
  this.handleSelection = (event) => {
37
45
  if (!this.steps.includes(event.target)) {
38
46
  return;
@@ -42,12 +50,27 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
42
50
  if (event.defaultPrevented || !this.reachable(index) || !this.valid(index)) {
43
51
  return;
44
52
  }
45
- this.requestActive(event.detail.value);
53
+ this.requestActive(keyOf(this.steps[index], index));
46
54
  });
47
55
  };
56
+ this.handleFocus = (event) => {
57
+ if (!this.arrows) {
58
+ return;
59
+ }
60
+ const path = event.composedPath();
61
+ const step = this.steps.find((one) => {
62
+ const name = one.shadowRoot?.querySelector('[part="label"]');
63
+ return name ? path.includes(name) : false;
64
+ });
65
+ if (step && step !== this.holder) {
66
+ this.holder = step;
67
+ this.rove();
68
+ }
69
+ };
48
70
  this.handleSlotChange = () => {
49
71
  this.sync();
50
72
  this.lock();
73
+ this.rove();
51
74
  };
52
75
  }
53
76
  static { this.words = 'helix-steps'; }
@@ -62,11 +85,20 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
62
85
  if (this.active === '') {
63
86
  return steps.findIndex((step) => step.current);
64
87
  }
65
- const item = this.resolveActive(steps);
88
+ const item = this.named(steps);
66
89
  return item ? steps.indexOf(item) : -1;
67
90
  }
91
+ named(steps) {
92
+ const keyed = steps.map((step, index) => ({
93
+ disabled: step.disabled,
94
+ step,
95
+ value: keyOf(step, index),
96
+ }));
97
+ return this.resolveActive(keyed)?.step;
98
+ }
68
99
  requestActive(next, cause) {
69
- const here = this.active === '' ? this.steps[this.at]?.value : undefined;
100
+ const at = this.active === '' ? this.at : -1;
101
+ const here = at === -1 ? undefined : keyOf(this.steps[at], at);
70
102
  if (here) {
71
103
  this.active = here;
72
104
  }
@@ -80,8 +112,8 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
80
112
  }
81
113
  reset() {
82
114
  const steps = this.steps;
83
- const first = steps.find((one) => !one.disabled);
84
- if (!first || !this.requestActive(first.value)) {
115
+ const first = steps.findIndex((one) => !one.disabled);
116
+ if (first === -1 || !this.requestActive(keyOf(steps[first], first))) {
85
117
  return false;
86
118
  }
87
119
  for (const one of steps) {
@@ -100,7 +132,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
100
132
  if (!target || !this.reachable(index) || !this.valid(index)) {
101
133
  return false;
102
134
  }
103
- return this.requestActive(target.value);
135
+ return this.requestActive(keyOf(target, index));
104
136
  }
105
137
  valid(index) {
106
138
  const from = this.steps[this.at];
@@ -128,8 +160,8 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
128
160
  return;
129
161
  }
130
162
  const steps = this.steps;
131
- const value = this.resolveActive(steps)?.value ?? '';
132
- const reached = steps.findIndex((step) => step.value === value);
163
+ const named = this.named(steps);
164
+ const reached = named ? steps.indexOf(named) : -1;
133
165
  steps.forEach((step, index) => {
134
166
  const current = index === reached;
135
167
  const passed = this.completion === 'passed' && index < reached;
@@ -141,11 +173,30 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
141
173
  });
142
174
  });
143
175
  }
176
+ rove() {
177
+ const steps = this.steps;
178
+ const open = steps.filter((step, index) => !step.disabled && this.reachable(index));
179
+ const current = steps[this.at];
180
+ const holder = !this.arrows
181
+ ? undefined
182
+ : this.holder && open.includes(this.holder)
183
+ ? this.holder
184
+ : current && open.includes(current)
185
+ ? current
186
+ : open[0];
187
+ for (const step of steps) {
188
+ whenHydrated(step, () => {
189
+ step.stop = !this.arrows || step === holder;
190
+ });
191
+ }
192
+ }
144
193
  connectedCallback() {
145
194
  super.connectedCallback();
146
195
  this.addEventListener('helix-select', this.handleSelection);
196
+ this.addEventListener('focusin', this.handleFocus);
147
197
  this.watcher = new MutationObserver(() => {
148
198
  this.lock();
199
+ this.rove();
149
200
  this.requestUpdate();
150
201
  });
151
202
  this.watcher.observe(this, {
@@ -157,6 +208,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
157
208
  disconnectedCallback() {
158
209
  super.disconnectedCallback();
159
210
  this.removeEventListener('helix-select', this.handleSelection);
211
+ this.removeEventListener('focusin', this.handleFocus);
160
212
  this.watcher?.disconnect();
161
213
  this.watcher = undefined;
162
214
  }
@@ -181,6 +233,15 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
181
233
  if (changed.has('linear') || changed.has('active') || changed.has('completion')) {
182
234
  this.lock();
183
235
  }
236
+ if (changed.has('active')) {
237
+ this.holder = undefined;
238
+ }
239
+ if (changed.has('arrows') ||
240
+ changed.has('linear') ||
241
+ changed.has('active') ||
242
+ changed.has('completion')) {
243
+ this.rove();
244
+ }
184
245
  }
185
246
  render() {
186
247
  const progress = progressOf(this.known);
@@ -106,7 +106,15 @@ export declare class HelixHeadlessStep extends HelixElement {
106
106
  */
107
107
  optional: boolean;
108
108
  /**
109
- * The value the step sends in `helix-select`.
109
+ * @internal Set by a sequence with `arrows`: whether the name holds the
110
+ * sequence's one tab stop. A property, because the sequence writes it after
111
+ * the step upgrades.
112
+ */
113
+ stop: boolean;
114
+ /**
115
+ * The value the step sends in `helix-select`, and what names the step in its
116
+ * sequence's `active`. Without one, the sequence names the step by its place,
117
+ * counted from 1.
110
118
  */
111
119
  value: string;
112
120
  /** Whether anything is slotted into `content`, which makes it a named group. */