@fusion.dev/helix-headless 0.153.0 → 0.155.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 (81) hide show
  1. package/custom-elements.json +1885 -140
  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/badge/root/badge-root.d.ts +15 -2
  7. package/dist/badge/root/badge-root.js +20 -3
  8. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  9. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  10. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  11. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  12. package/dist/button-group/root/button-group-root.d.ts +2 -0
  13. package/dist/button-group/root/button-group-root.js +1 -0
  14. package/dist/card/root/card-root.d.ts +31 -11
  15. package/dist/card/root/card-root.js +25 -11
  16. package/dist/code/root/code-root.d.ts +7 -0
  17. package/dist/code/root/code-root.js +1 -1
  18. package/dist/code-block/root/code-block-root.d.ts +37 -1
  19. package/dist/code-block/root/code-block-root.js +38 -5
  20. package/dist/color-input/root/color-input-root.d.ts +9 -3
  21. package/dist/color-input/root/color-input-root.js +3 -1
  22. package/dist/color-picker/channels.d.ts +5 -3
  23. package/dist/color-picker/channels.js +79 -21
  24. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  25. package/dist/color-picker/root/color-picker-root.js +75 -13
  26. package/dist/date-input/root/date-input-root.d.ts +1 -0
  27. package/dist/date-input/root/date-input-root.js +2 -1
  28. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  29. package/dist/date-picker/root/date-picker-root.js +54 -31
  30. package/dist/element-events.generated.js +2 -1
  31. package/dist/empty/root/empty-root.d.ts +19 -1
  32. package/dist/empty/root/empty-root.js +13 -4
  33. package/dist/icon/fetch.d.ts +20 -0
  34. package/dist/icon/fetch.js +70 -0
  35. package/dist/icon/root/icon-root.d.ts +27 -5
  36. package/dist/icon/root/icon-root.js +41 -17
  37. package/dist/navigation/root/navigation-root.d.ts +26 -0
  38. package/dist/navigation/root/navigation-root.js +58 -1
  39. package/dist/quote/root/quote-root.d.ts +17 -1
  40. package/dist/quote/root/quote-root.js +15 -2
  41. package/dist/select/root/select-root.d.ts +29 -3
  42. package/dist/select/root/select-root.js +34 -9
  43. package/dist/skeleton/root/skeleton-root.d.ts +7 -0
  44. package/dist/skeleton/root/skeleton-root.js +4 -1
  45. package/dist/slider/root/slider-root.d.ts +71 -9
  46. package/dist/slider/root/slider-root.js +129 -42
  47. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  48. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  49. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  50. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  51. package/dist/splitter/root/splitter-root.d.ts +22 -0
  52. package/dist/splitter/root/splitter-root.js +36 -7
  53. package/dist/stat/group/stat-group.d.ts +23 -4
  54. package/dist/stat/group/stat-group.js +13 -1
  55. package/dist/stat/root/stat-root.d.ts +46 -1
  56. package/dist/stat/root/stat-root.js +29 -5
  57. package/dist/steps/reach.d.ts +5 -1
  58. package/dist/steps/reach.js +2 -2
  59. package/dist/steps/root/steps-root.d.ts +37 -2
  60. package/dist/steps/root/steps-root.js +49 -7
  61. package/dist/steps/step/step.d.ts +6 -0
  62. package/dist/steps/step/step.js +4 -0
  63. package/dist/tabs/root/tabs-root.d.ts +37 -0
  64. package/dist/tabs/root/tabs-root.js +92 -2
  65. package/dist/tabs/tab/tab.d.ts +38 -2
  66. package/dist/tabs/tab/tab.js +45 -4
  67. package/dist/tag/group/tag-group.d.ts +29 -3
  68. package/dist/tag/group/tag-group.js +42 -11
  69. package/dist/toggle/root/toggle-root.d.ts +2 -0
  70. package/dist/toggle/root/toggle-root.js +1 -1
  71. package/dist/tree/item/tree-item.d.ts +10 -0
  72. package/dist/tree/item/tree-item.js +12 -0
  73. package/dist/tree/root/tree-root.d.ts +40 -1
  74. package/dist/tree/root/tree-root.js +120 -4
  75. package/dist/tree/selection.d.ts +6 -0
  76. package/dist/tree/selection.js +11 -0
  77. package/lint.json +1 -1
  78. package/package.json +5 -5
  79. package/types/jsx.d.ts +1412 -207
  80. package/types/vue.d.ts +294 -65
  81. package/types/words.d.ts +49 -1
@@ -35,6 +35,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
35
35
  * takes all of the change, as Web Awesome's split panel does. Without it,
36
36
  * both panes keep their share.
37
37
  *
38
+ * **Three or more panes along one axis are splitters nested on that axis**, a
39
+ * splitter in the `end` pane of another for three columns. The nested one
40
+ * keeps the pane on its far side the size it was while the outer divider
41
+ * moves, so a divider resizes only the two panes beside it, as Zag's splitter
42
+ * does with any number of panels. Where the whole row is resized, every pane
43
+ * keeps its share, unless `primary` says otherwise.
44
+ *
38
45
  * **Keyboard.** The divider is one tab stop.
39
46
  *
40
47
  * | Key | What it does |
@@ -213,6 +220,21 @@ export declare class HelixHeadlessSplitter extends HelixElement {
213
220
  primary?: 'end' | 'start';
214
221
  /** Watches this element's length along its axis while `primary` is set. */
215
222
  private lengths;
223
+ /**
224
+ * The splitter this one is nested in on the same axis, and the pane of it
225
+ * this one is in, for a row of three or more panes.
226
+ */
227
+ private chain;
228
+ /** The length of the splitter this one is chained to, last seen. */
229
+ private chainLength;
230
+ /**
231
+ * The pane that keeps its pixels as this splitter's length changes:
232
+ * `primary`, or the far pane of one chained into another, whose divider
233
+ * would otherwise drag this one's along with it.
234
+ */
235
+ private get held();
236
+ /** The chained splitter's length along the shared axis, in pixels. */
237
+ private measureChain;
216
238
  /** The length along the axis last seen, in pixels. */
217
239
  private length;
218
240
  /**
@@ -70,8 +70,15 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
70
70
  this.reached = undefined;
71
71
  this.requestUpdate('position', previous);
72
72
  }
73
+ get held() {
74
+ return this.primary ?? this.chain?.side;
75
+ }
76
+ measureChain() {
77
+ const box = this.chain?.by.getBoundingClientRect();
78
+ return box ? (this.vertical ? box.height : box.width) : 0;
79
+ }
73
80
  watchLength() {
74
- if (!this.primary || typeof ResizeObserver === 'undefined') {
81
+ if (!this.held || typeof ResizeObserver === 'undefined') {
75
82
  this.lengths?.disconnect();
76
83
  this.lengths = undefined;
77
84
  this.length = 0;
@@ -92,17 +99,26 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
92
99
  holdPrimary(length) {
93
100
  const before = this.length;
94
101
  this.length = length;
95
- if (!this.primary || before <= 0 || length <= 0 || length === before) {
102
+ const held = this.held;
103
+ if (!this.primary && this.chain) {
104
+ const outer = this.measureChain();
105
+ const outerBefore = this.chainLength;
106
+ this.chainLength = outer;
107
+ if (outer !== outerBefore) {
108
+ return;
109
+ }
110
+ }
111
+ if (!held || before <= 0 || length <= 0 || length === before) {
96
112
  return;
97
113
  }
98
114
  if (this.collapsed || this.dragging) {
99
115
  return;
100
116
  }
101
117
  const position = this.position;
102
- const kept = ((this.primary === 'start' ? position : 100 - position) / 100) * before;
118
+ const kept = ((held === 'start' ? position : 100 - position) / 100) * before;
103
119
  const share = (kept / length) * 100;
104
120
  const previous = position;
105
- this.positionState = this.clamp(this.primary === 'start' ? share : 100 - share);
121
+ this.positionState = this.clamp(held === 'start' ? share : 100 - share);
106
122
  this.requestUpdate('position', previous);
107
123
  }
108
124
  get showing() {
@@ -261,6 +277,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
261
277
  });
262
278
  this.max = 100;
263
279
  this.min = 0;
280
+ this.chain = null;
281
+ this.chainLength = 0;
264
282
  this.length = 0;
265
283
  this.handlePaneKeys = (event) => {
266
284
  if (event.key !== 'F6' ||
@@ -484,7 +502,17 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
484
502
  }
485
503
  findOuter() {
486
504
  const outer = closestInstance(this.parentElement, HelixHeadlessSplitter_1);
487
- if (!outer || outer === this || outer.vertical === this.vertical) {
505
+ this.chain = null;
506
+ if (outer && outer !== this && outer.vertical === this.vertical) {
507
+ let node = this;
508
+ while (node.parentElement && node.parentElement !== outer) {
509
+ node = node.parentElement;
510
+ }
511
+ this.chain = { by: outer, side: node.slot === 'start' ? 'start' : 'end' };
512
+ this.chainLength = this.measureChain();
513
+ return;
514
+ }
515
+ if (!outer || outer === this) {
488
516
  return;
489
517
  }
490
518
  this.outer = outer;
@@ -499,8 +527,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
499
527
  super.connectedCallback();
500
528
  if (this.hasUpdated) {
501
529
  this.watchWidth();
502
- this.watchLength();
503
530
  this.findOuter();
531
+ this.watchLength();
504
532
  }
505
533
  this.addEventListener('keydown', this.handlePaneKeys);
506
534
  }
@@ -512,6 +540,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
512
540
  this.lengths?.disconnect();
513
541
  this.lengths = undefined;
514
542
  this.length = 0;
543
+ this.chain = null;
515
544
  if (this.outer) {
516
545
  channel.get(this.outer)?.uncross(this);
517
546
  }
@@ -521,8 +550,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
521
550
  firstUpdated(changed) {
522
551
  super.firstUpdated(changed);
523
552
  this.watchWidth();
524
- this.watchLength();
525
553
  this.findOuter();
554
+ this.watchLength();
526
555
  this.finishHydration();
527
556
  }
528
557
  updated(changed) {
@@ -4,9 +4,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
4
4
  * @summary A row of related measurements.
5
5
  *
6
6
  * @description
7
- * Screen readers announce the stats as a group. The figures are read together
8
- * and their order has no meaning, so the group does not announce positions such
9
- * as "1 of 4".
7
+ * **A named group is a group**, so a screen reader announces the stats
8
+ * together under its name, such as "Usage, group". Name it with `label`,
9
+ * `aria-label` or `aria-labelledby`. An unnamed one has no role, since a group
10
+ * with no name only adds "group" to what a reader says, as an unnamed Card
11
+ * has none. The figures are read together and their order has no meaning, so
12
+ * the group does not announce positions such as "1 of 4".
10
13
  *
11
14
  * @since 0.1.0
12
15
  * @status stable
@@ -17,9 +20,25 @@ import { type PropertyValues, type TemplateResult } from 'lit';
17
20
  */
18
21
  export declare class HelixHeadlessStatGroup extends HelixElement {
19
22
  /**
20
- * Accessible name for the group.
23
+ * Accessible name for the group, which makes it a group.
24
+ *
25
+ * Absent leaves the stats with no group around them, unless `aria-label` or
26
+ * `aria-labelledby` names it.
21
27
  */
22
28
  label: string;
29
+ /**
30
+ * Hears a name written on the element, which no property mirrors.
31
+ *
32
+ * Lit observes the attributes of its own properties only, and `ariaLabel`
33
+ * and `ariaLabelledByElements` belong to the platform.
34
+ */
35
+ static get observedAttributes(): string[];
36
+ /**
37
+ * Called by the browser when an observed attribute changes. A name written
38
+ * or taken off asks for an update, because it decides whether the stats are
39
+ * a group.
40
+ */
41
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
23
42
  protected willUpdate(changed: PropertyValues): void;
24
43
  protected render(): TemplateResult;
25
44
  }
@@ -12,9 +12,21 @@ let HelixHeadlessStatGroup = class HelixHeadlessStatGroup extends HelixElement {
12
12
  super(...arguments);
13
13
  this.label = '';
14
14
  }
15
+ static get observedAttributes() {
16
+ return [...super.observedAttributes, 'aria-label', 'aria-labelledby'];
17
+ }
18
+ attributeChangedCallback(name, old, value) {
19
+ super.attributeChangedCallback(name, old, value);
20
+ if (name === 'aria-label' || name === 'aria-labelledby') {
21
+ this.requestUpdate();
22
+ }
23
+ }
15
24
  willUpdate(changed) {
16
25
  super.willUpdate(changed);
17
- hostAria(this.internals, { label: this.label, role: 'group' });
26
+ const named = this.label.trim() !== '' ||
27
+ Boolean(this.getAttribute?.('aria-label')?.trim()) ||
28
+ Boolean(this.getAttribute?.('aria-labelledby')?.trim());
29
+ hostAria(this.internals, { label: this.label, role: named ? 'group' : '' });
18
30
  }
19
31
  render() {
20
32
  return html `<slot></slot>`;
@@ -19,6 +19,19 @@ import { type PropertyValues, type TemplateResult } from 'lit';
19
19
  * attribute; a stat with no label has no role. Put a `Trend` in the `trend`
20
20
  * slot to have it read inside the group, after the figure it qualifies.
21
21
  *
22
+ * **Or give the figure as a number.** `value` is written with
23
+ * `Intl.NumberFormat`, in `locale` and with `format-options`, so
24
+ * `value="1234567" format-options='{"notation":"compact"}'` shows `1.2M`, and
25
+ * a server writes it as the browser does. A number in `value` takes the place
26
+ * of a slotted figure.
27
+ *
28
+ * **Each slot is a part of the same name**, and `value` for the figure, so a
29
+ * page reaches the label, the unit and the trend from outside. A slot draws no
30
+ * box, so a part passes inherited properties to what is slotted. **The stat
31
+ * says which pieces it holds**, as `:state(has-label)`, `:state(has-unit)` and
32
+ * `:state(has-trend)`, read in the browser, so a served stat carries none
33
+ * until the script runs.
34
+ *
22
35
  * @since 0.1.0
23
36
  * @status stable
24
37
  * @category data
@@ -30,7 +43,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
30
43
  * @slot trend - How the value moved, usually a `Trend`, read after the value inside the stat's group.
31
44
  * @slot unit - The unit, shown beside the value.
32
45
  *
33
- * @csspart value - The slot the value is written in. A slot draws no box, so it takes the inherited properties, such as color and font, and passes them to the value.
46
+ * @csspart label - The slot the label arrives through, drawing no box, as `value` does.
47
+ * @csspart trend - The slot the trend arrives through, drawing no box, as `value` does.
48
+ * @csspart unit - The slot the unit arrives through, drawing no box, as `value` does.
49
+ * @csspart value - The slot the value is written in, or the figure `value` writes. A slot draws no box, so it takes the inherited properties, such as color and font, and passes them to the value.
50
+ *
51
+ * @cssstate has-label - Something is slotted into `label`.
52
+ * @cssstate has-trend - Something is slotted into `trend`.
53
+ * @cssstate has-unit - Something is slotted into `unit`.
34
54
  */
35
55
  export declare class HelixHeadlessStat extends HelixElement {
36
56
  /**
@@ -61,9 +81,34 @@ export declare class HelixHeadlessStat extends HelixElement {
61
81
  * carries nothing nobody wrote.
62
82
  */
63
83
  direction?: 'column-reverse' | 'row' | 'row-reverse';
84
+ /**
85
+ * `Intl.NumberFormat` options `value` is written with, such as
86
+ * `{"notation":"compact"}` for `1.2M` or `{"style":"percent"}` for `98%`,
87
+ * written as JSON in the attribute.
88
+ *
89
+ * Absent writes the number as the locale writes any number.
90
+ */
91
+ formatOptions?: Intl.NumberFormatOptions;
92
+ /**
93
+ * The locale `value` is written in, such as `de` for `1.234.567`.
94
+ *
95
+ * Absent reads the nearest `lang`, then the runtime's own. Set it on a page
96
+ * rendered on a server, so the server and the browser write the figure
97
+ * alike.
98
+ */
99
+ locale: string;
100
+ /**
101
+ * The figure as a number, written with `format-options` in `locale` in
102
+ * place of a slotted value.
103
+ *
104
+ * Absent leaves the figure to the slot.
105
+ */
106
+ value?: number;
64
107
  protected willUpdate(changed: PropertyValues): void;
65
108
  protected firstUpdated(changed: PropertyValues): void;
66
109
  protected updated(changed: PropertyValues): void;
110
+ /** Says which pieces the stat holds, one call apiece so each pairs with its `@cssstate`. */
111
+ private readonly readPieces;
67
112
  protected render(): TemplateResult;
68
113
  }
69
114
  declare global {
@@ -4,10 +4,20 @@ 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, reportRuntime, slottedWords, } from '@fusion.dev/helix-core';
7
+ import { customElement, formatNumber, HelixElement, hostAria, reportRuntime, slotFilled, slottedWords, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessStat = class HelixHeadlessStat extends HelixElement {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.locale = '';
14
+ this.readPieces = () => {
15
+ const filled = (name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`));
16
+ this.toggleState('has-label', filled('label'));
17
+ this.toggleState('has-unit', filled('unit'));
18
+ this.toggleState('has-trend', filled('trend'));
19
+ };
20
+ }
11
21
  willUpdate(changed) {
12
22
  super.willUpdate(changed);
13
23
  const labels = this.knownChildren.filter((child) => child.slot === 'label');
@@ -25,6 +35,7 @@ let HelixHeadlessStat = class HelixHeadlessStat extends HelixElement {
25
35
  }
26
36
  updated(changed) {
27
37
  super.updated(changed);
38
+ this.readPieces();
28
39
  const given = this.direction;
29
40
  if (changed.has('direction') && (given === 'up' || given === 'down' || given === 'neutral')) {
30
41
  reportRuntime({
@@ -37,15 +48,28 @@ let HelixHeadlessStat = class HelixHeadlessStat extends HelixElement {
37
48
  }
38
49
  }
39
50
  render() {
40
- return html `<slot name="label" @slotchange=${() => this.requestUpdate()}></slot>
41
- <slot part="value"></slot>
42
- <slot name="unit"></slot>
43
- <slot name="trend"></slot>`;
51
+ const value = this.value;
52
+ const figure = value === undefined || !Number.isFinite(value)
53
+ ? ''
54
+ : formatNumber(value, wordsLocale(this), this.formatOptions);
55
+ return html `<slot name="label" part="label" @slotchange=${() => this.requestUpdate()}></slot>
56
+ ${figure === '' ? html `<slot part="value"></slot>` : html `<span part="value">${figure}</span>`}
57
+ <slot name="unit" part="unit" @slotchange=${this.readPieces}></slot>
58
+ <slot name="trend" part="trend" @slotchange=${this.readPieces}></slot>`;
44
59
  }
45
60
  };
46
61
  __decorate([
47
62
  property({ reflect: true })
48
63
  ], HelixHeadlessStat.prototype, "direction", void 0);
64
+ __decorate([
65
+ property({ attribute: 'format-options', type: Object })
66
+ ], HelixHeadlessStat.prototype, "formatOptions", void 0);
67
+ __decorate([
68
+ property()
69
+ ], HelixHeadlessStat.prototype, "locale", void 0);
70
+ __decorate([
71
+ property({ type: Number })
72
+ ], HelixHeadlessStat.prototype, "value", void 0);
49
73
  HelixHeadlessStat = __decorate([
50
74
  customElement('helix-headless-stat')
51
75
  ], HelixHeadlessStat);
@@ -11,8 +11,12 @@ export interface ReachableStep {
11
11
  * Back is always allowed, as Ark UI's linear steps allow it. Forward is allowed
12
12
  * only where every stage from the one the reader is on up to the target is
13
13
  * complete or optional, so no stage is passed unfinished.
14
+ *
15
+ * `passing` is a sequence whose stages are complete once a reader moves past
16
+ * them, so the stage the reader is on counts as finished and the next one is
17
+ * in reach.
14
18
  */
15
- export declare const canReach: (steps: readonly ReachableStep[], from: number, to: number) => boolean;
19
+ export declare const canReach: (steps: readonly ReachableStep[], from: number, to: number, passing?: boolean) => boolean;
16
20
  /**
17
21
  * How far through a sequence is, from 0 to 100: the share of its stages that
18
22
  * are complete, rounded to a whole number. An empty sequence is at 0.
@@ -1,8 +1,8 @@
1
- export const canReach = (steps, from, to) => {
1
+ export const canReach = (steps, from, to, passing = false) => {
2
2
  if (to <= from) {
3
3
  return true;
4
4
  }
5
- for (let at = Math.max(from, 0); at < to; at += 1) {
5
+ for (let at = Math.max(passing ? from + 1 : from, 0); at < to; at += 1) {
6
6
  const step = steps[at];
7
7
  if (step && !step.complete && !step.optional) {
8
8
  return false;
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { 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;
@@ -18,7 +18,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
18
18
  * UI's linear steps do: a stage ahead is out of reach, and its button disabled,
19
19
  * until every stage before it is `complete` or `optional`. Going back is always
20
20
  * allowed. `next()` and `previous()` move one stage either way, under the
21
- * same rules, for your own buttons.
21
+ * 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.
23
+ *
24
+ * `completion="passed"` marks a stage `complete` once a reader moves past it,
25
+ * as Ark UI counts a stage before the current one as completed. Going back
26
+ * leaves it complete. In a `linear` sequence the next stage is then in reach
27
+ * from the current one, and `validate` decides whether the reader may go.
22
28
  *
23
29
  * To check a stage before a reader moves on from it, set `validate` to a
24
30
  * function that answers whether the stage is valid, as Ark UI's `isStepValid`
@@ -39,6 +45,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
39
45
  * current stage's button says `aria-current="step"`, and a finished stage's
40
46
  * button is described by its `complete-label`.
41
47
  *
48
+ * **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.
54
+ *
42
55
  * @since 0.1.0
43
56
  * @status stable
44
57
  * @category navigation
@@ -63,6 +76,20 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
63
76
  * `registerWords('de', { 'helix-steps': { progressLabel: '{share, number, percent} erledigt' } })`.
64
77
  */
65
78
  static words: string;
79
+ /**
80
+ * 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.
82
+ */
83
+ arrows: boolean;
84
+ /**
85
+ * What marks a stage `complete`.
86
+ *
87
+ * Absent, you do: each step's `complete` is yours, because a reader who
88
+ * steps back has finished more stages than the one they are on. `passed`
89
+ * marks every stage before the one a reader moves to, and leaves the marks
90
+ * when they go back.
91
+ */
92
+ completion?: 'passed';
66
93
  /**
67
94
  * The sequence's name, read by screen readers.
68
95
  *
@@ -120,6 +147,7 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
120
147
  * a reader is told to come back to by number.
121
148
  */
122
149
  variant?: 'numeric';
150
+ protected readonly keys: KeyboardNavigationController;
123
151
  /** The steps, in the order the consumer wrote them. */
124
152
  private get steps();
125
153
  /** The index of the stage the reader is on, or `-1`. */
@@ -132,6 +160,13 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
132
160
  next(): boolean;
133
161
  /** Moves to the stage before, as Ark UI's `goToPrevStep` does. Returns whether it moved. */
134
162
  previous(): boolean;
163
+ /**
164
+ * Goes back to the first stage that can be gone to, and clears `complete`
165
+ * and the invalid mark from every stage, as Ark UI's `resetStep` does.
166
+ * Returns whether it moved: a listener can refuse the move with
167
+ * `helix-before-activate`, and the stages then keep their marks.
168
+ */
169
+ reset(): boolean;
135
170
  /** Moves `delta` stages, skipping disabled ones. */
136
171
  private step;
137
172
  /**
@@ -4,16 +4,35 @@ 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, formatWords, HelixElement, hostAria, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { 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';
11
+ const navigable = (step) => {
12
+ const label = () => step.shadowRoot?.querySelector('[part="label"]');
13
+ return focusItemOf(step, {
14
+ disabled: step.disabled || step.locked,
15
+ focus: () => step.focus(),
16
+ owns: (node) => {
17
+ const control = label();
18
+ return Boolean(control && node && (node === control || control.contains(node)));
19
+ },
20
+ });
21
+ };
11
22
  let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
12
23
  constructor() {
13
24
  super(...arguments);
25
+ this.arrows = false;
14
26
  this.label = 'Steps';
15
27
  this.linear = false;
16
28
  this.progressLabel = '{share, number, percent} complete';
29
+ this.keys = new KeyboardNavigationController(this, {
30
+ items: () => (this.arrows ? this.steps.map(navigable) : []),
31
+ origin: 'focus',
32
+ reserved: () => this.orientation === 'vertical' ? ['ArrowLeft', 'ArrowRight'] : ['ArrowDown', 'ArrowUp'],
33
+ selects: () => false,
34
+ wrap: false,
35
+ });
17
36
  this.handleSelection = (event) => {
18
37
  if (event.defaultPrevented || !this.steps.includes(event.target)) {
19
38
  return;
@@ -45,6 +64,18 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
45
64
  previous() {
46
65
  return this.step(-1);
47
66
  }
67
+ reset() {
68
+ const steps = this.steps;
69
+ const first = steps.find((one) => !one.disabled);
70
+ if (!first || !this.requestActive(first.value)) {
71
+ return false;
72
+ }
73
+ for (const one of steps) {
74
+ one.complete = false;
75
+ one.invalid = false;
76
+ }
77
+ return true;
78
+ }
48
79
  step(delta) {
49
80
  const steps = this.steps;
50
81
  let index = this.at + delta;
@@ -67,7 +98,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
67
98
  return valid;
68
99
  }
69
100
  reachable(index) {
70
- return !this.linear || canReach(this.steps, this.at, index);
101
+ return !this.linear || canReach(this.steps, this.at, index, this.completion === 'passed');
71
102
  }
72
103
  lock() {
73
104
  const steps = this.steps;
@@ -84,12 +115,17 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
84
115
  }
85
116
  const steps = this.steps;
86
117
  const value = this.resolveActive(steps)?.value ?? '';
87
- for (const step of steps) {
88
- const current = step.value === value;
118
+ const reached = steps.findIndex((step) => step.value === value);
119
+ steps.forEach((step, index) => {
120
+ const current = index === reached;
121
+ const passed = this.completion === 'passed' && index < reached;
89
122
  whenHydrated(step, () => {
90
123
  step.current = current;
124
+ if (passed) {
125
+ step.complete = true;
126
+ }
91
127
  });
92
- }
128
+ });
93
129
  }
94
130
  connectedCallback() {
95
131
  super.connectedCallback();
@@ -125,10 +161,10 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
125
161
  }
126
162
  updated(changed) {
127
163
  super.updated(changed);
128
- if (changed.has('active')) {
164
+ if (changed.has('active') || changed.has('completion')) {
129
165
  this.sync();
130
166
  }
131
- if (changed.has('linear') || changed.has('active')) {
167
+ if (changed.has('linear') || changed.has('active') || changed.has('completion')) {
132
168
  this.lock();
133
169
  }
134
170
  }
@@ -148,6 +184,12 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
148
184
  ></div>`;
149
185
  }
150
186
  };
187
+ __decorate([
188
+ property({ type: Boolean })
189
+ ], HelixHeadlessSteps.prototype, "arrows", void 0);
190
+ __decorate([
191
+ property({ reflect: true })
192
+ ], HelixHeadlessSteps.prototype, "completion", void 0);
151
193
  __decorate([
152
194
  words,
153
195
  property()
@@ -53,6 +53,7 @@ import '../../button/index.js';
53
53
  * @csspart state - The words that describe a finished stage's button. Hidden.
54
54
  *
55
55
  * @cssstate invalid - The sequence's `validate` last answered that this stage is not valid.
56
+ * @cssstate locked - A `linear` sequence holds this stage out of reach until the stages before it are finished.
56
57
  */
57
58
  export declare class HelixHeadlessStep extends HelixElement {
58
59
  /**
@@ -106,6 +107,11 @@ export declare class HelixHeadlessStep extends HelixElement {
106
107
  value: string;
107
108
  /** Says a stage was finished, from the first time one is. */
108
109
  private readonly status;
110
+ /**
111
+ * Puts focus on the step's name, the control a reader presses, as a
112
+ * `focus()` on a native control would.
113
+ */
114
+ focus(options?: FocusOptions): void;
109
115
  /**
110
116
  * Fires `helix-select` for the step, unless the step is `disabled` or a
111
117
  * linear sequence has it out of reach.
@@ -26,6 +26,9 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
26
26
  this.status = new StatusController(this);
27
27
  }
28
28
  static { this.words = 'helix-step'; }
29
+ focus(options) {
30
+ this.partNamed('label')?.focus(options);
31
+ }
29
32
  select() {
30
33
  if (this.disabled || this.locked) {
31
34
  return;
@@ -44,6 +47,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
44
47
  super.willUpdate(changed);
45
48
  hostAria(this.internals, { role: 'listitem' });
46
49
  this.toggleState('invalid', this.invalid);
50
+ this.toggleState('locked', this.locked);
47
51
  }
48
52
  updated(changed) {
49
53
  super.updated(changed);