@fusion.dev/helix-headless 0.154.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 (59) hide show
  1. package/custom-elements.json +1427 -88
  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/color-input/root/color-input-root.d.ts +9 -3
  13. package/dist/color-input/root/color-input-root.js +3 -1
  14. package/dist/color-picker/channels.d.ts +5 -3
  15. package/dist/color-picker/channels.js +79 -21
  16. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  17. package/dist/color-picker/root/color-picker-root.js +75 -13
  18. package/dist/date-input/root/date-input-root.d.ts +1 -0
  19. package/dist/date-input/root/date-input-root.js +2 -1
  20. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  21. package/dist/date-picker/root/date-picker-root.js +54 -31
  22. package/dist/element-events.generated.js +2 -1
  23. package/dist/navigation/root/navigation-root.d.ts +26 -0
  24. package/dist/navigation/root/navigation-root.js +58 -1
  25. package/dist/select/root/select-root.d.ts +29 -3
  26. package/dist/select/root/select-root.js +34 -9
  27. package/dist/slider/root/slider-root.d.ts +71 -9
  28. package/dist/slider/root/slider-root.js +129 -42
  29. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  30. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  31. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  32. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  33. package/dist/splitter/root/splitter-root.d.ts +22 -0
  34. package/dist/splitter/root/splitter-root.js +36 -7
  35. package/dist/steps/reach.d.ts +5 -1
  36. package/dist/steps/reach.js +2 -2
  37. package/dist/steps/root/steps-root.d.ts +37 -2
  38. package/dist/steps/root/steps-root.js +49 -7
  39. package/dist/steps/step/step.d.ts +6 -0
  40. package/dist/steps/step/step.js +4 -0
  41. package/dist/tabs/root/tabs-root.d.ts +37 -0
  42. package/dist/tabs/root/tabs-root.js +92 -2
  43. package/dist/tabs/tab/tab.d.ts +38 -2
  44. package/dist/tabs/tab/tab.js +45 -4
  45. package/dist/tag/group/tag-group.d.ts +29 -3
  46. package/dist/tag/group/tag-group.js +42 -11
  47. package/dist/toggle/root/toggle-root.d.ts +2 -0
  48. package/dist/toggle/root/toggle-root.js +1 -1
  49. package/dist/tree/item/tree-item.d.ts +10 -0
  50. package/dist/tree/item/tree-item.js +12 -0
  51. package/dist/tree/root/tree-root.d.ts +40 -1
  52. package/dist/tree/root/tree-root.js +120 -4
  53. package/dist/tree/selection.d.ts +6 -0
  54. package/dist/tree/selection.js +11 -0
  55. package/lint.json +1 -1
  56. package/package.json +5 -5
  57. package/types/jsx.d.ts +984 -151
  58. package/types/vue.d.ts +221 -49
  59. package/types/words.d.ts +36 -1
@@ -12,7 +12,11 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
12
12
  constructor() {
13
13
  super(...arguments);
14
14
  this.disabled = false;
15
+ this.download = false;
16
+ this.href = '';
17
+ this.rel = '';
15
18
  this.shortcut = '';
19
+ this.target = '';
16
20
  this.value = '';
17
21
  this.isSpeedDialItem = true;
18
22
  this.hasShortcut = false;
@@ -22,6 +26,16 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
22
26
  this.handleClick = () => {
23
27
  this.choose();
24
28
  };
29
+ this.handleKeydown = (event) => {
30
+ if (event.key !== ' ') {
31
+ return;
32
+ }
33
+ const link = this.renderRoot.querySelector('a[part="item"]');
34
+ if (link) {
35
+ event.preventDefault();
36
+ link.click();
37
+ }
38
+ };
25
39
  }
26
40
  focus(options) {
27
41
  this.partNamed('item')?.focus(options);
@@ -55,6 +69,27 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
55
69
  }
56
70
  render() {
57
71
  const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
72
+ const inside = html `<span part="icon"><slot name="icon"></slot></span>
73
+ <span part="label"><slot></slot><span
74
+ part="shortcut"
75
+ aria-hidden="true"
76
+ ?hidden=${!this.hasShortcut && this.shortcut === ''}
77
+ >${this.renderShortcut()}</span
78
+ ></span>`;
79
+ if (this.href !== '' && !this.disabled) {
80
+ return html `<a
81
+ part="item"
82
+ href=${this.href}
83
+ target=${this.target || nothing}
84
+ rel=${this.rel || nothing}
85
+ ?download=${this.download}
86
+ role="menuitem"
87
+ aria-keyshortcuts=${keys}
88
+ tabindex="-1"
89
+ @click=${this.handleClick}
90
+ @keydown=${this.handleKeydown}
91
+ >${inside}</a>`;
92
+ }
58
93
  return html `<button
59
94
  part="item"
60
95
  role="menuitem"
@@ -65,13 +100,7 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
65
100
  tabindex="-1"
66
101
  @click=${this.handleClick}
67
102
  >
68
- <span part="icon"><slot name="icon"></slot></span>
69
- <span part="label"><slot></slot><span
70
- part="shortcut"
71
- aria-hidden="true"
72
- ?hidden=${!this.hasShortcut && this.shortcut === ''}
73
- >${this.renderShortcut()}</span
74
- ></span>
103
+ ${inside}
75
104
  </button>`;
76
105
  }
77
106
  renderShortcut() {
@@ -89,9 +118,21 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
89
118
  __decorate([
90
119
  property({ reflect: true, type: Boolean })
91
120
  ], HelixHeadlessSpeedDialItem.prototype, "disabled", void 0);
121
+ __decorate([
122
+ property({ type: Boolean })
123
+ ], HelixHeadlessSpeedDialItem.prototype, "download", void 0);
124
+ __decorate([
125
+ property()
126
+ ], HelixHeadlessSpeedDialItem.prototype, "href", void 0);
127
+ __decorate([
128
+ property()
129
+ ], HelixHeadlessSpeedDialItem.prototype, "rel", void 0);
92
130
  __decorate([
93
131
  property()
94
132
  ], HelixHeadlessSpeedDialItem.prototype, "shortcut", void 0);
133
+ __decorate([
134
+ property()
135
+ ], HelixHeadlessSpeedDialItem.prototype, "target", void 0);
95
136
  __decorate([
96
137
  property()
97
138
  ], HelixHeadlessSpeedDialItem.prototype, "value", void 0);
@@ -25,8 +25,9 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
25
25
  * action whose name starts with it. The actions are one tab stop, and Tab
26
26
  * closes the dial and moves on.
27
27
  *
28
- * **Keyboard.** On the trigger, Enter, Space or a press opens the dial on its
29
- * first action. With the dial open:
28
+ * **Keyboard.** On the trigger, Enter, Space, Down or a press opens the dial
29
+ * on its first action, and Up opens it on its last, as an APG menu button
30
+ * does. With the dial open:
30
31
  *
31
32
  * | Key | What it does |
32
33
  * |---|---|
@@ -50,6 +51,13 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
50
51
  * and a slotted one alike. A press that has opened the dial does not also do
51
52
  * the trigger's action.
52
53
  *
54
+ * **Open on hover.** With `open-on="hover"`, a mouse resting on the trigger
55
+ * opens the dial, as Material's speed dial does, and the dial closes a moment
56
+ * after the mouse leaves both the trigger and the actions. A press, a touch
57
+ * and the keyboard open it as under `click`, and a press on the trigger of a
58
+ * dial already open moves focus to its first action, so the press a mouse
59
+ * makes after hovering keeps the dial open.
60
+ *
53
61
  * @since 0.1.0
54
62
  * @status stable
55
63
  * @category actions
@@ -57,6 +65,7 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
57
65
  * @documentation https://fusion.dev/helix/components/speed-dial
58
66
  *
59
67
  * @slot - The actions.
68
+ * @slot icon - Replaces the plus on the button the dial draws when no trigger is slotted, such as with a pencil for a dial of ways to write.
60
69
  * @slot trigger - The control that opens the actions. Without one, a button named by `label` is drawn.
61
70
  *
62
71
  * @event {HelixBeforeCloseEvent} helix-before-close - The actions are about to close. Cancelable: cancel it to keep them open.
@@ -94,11 +103,13 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
94
103
  */
95
104
  offset: number;
96
105
  /**
97
- * What opens the dial. `click` opens it on a press of the trigger. Under
98
- * `long-press`, a press is the trigger's own action, and the dial opens on a
99
- * press held for half a second, or on Alt+Down or Alt+Up.
106
+ * What opens the dial. `click` opens it on a press of the trigger. `hover`
107
+ * also opens it while a mouse rests on the trigger, and closes it once the
108
+ * mouse has left. Under `long-press`, a press is the trigger's own action,
109
+ * and the dial opens on a press held for half a second, or on Alt+Down or
110
+ * Alt+Up.
100
111
  */
101
- openOn: 'click' | 'long-press';
112
+ openOn: 'click' | 'hover' | 'long-press';
102
113
  /**
103
114
  * The direction the actions open in. They move to another side if that side
104
115
  * does not fit.
@@ -117,6 +128,10 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
117
128
  * wherever the reader is typing.
118
129
  */
119
130
  private focusOnOpen;
131
+ /** Whether the open under way puts focus on the last action, from Up. */
132
+ private focusLast;
133
+ /** The close a mouse leaving a hover dial has asked for, a moment from now. */
134
+ private leaving;
120
135
  protected readonly panel: PopoverController;
121
136
  /** Whatever the consumer slotted, or the button rendered in its place. */
122
137
  private get trigger();
@@ -128,6 +143,17 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
128
143
  /** Watches the trigger for a held press, under `open-on="long-press"`. */
129
144
  private readonly longPress;
130
145
  private get pressHeld();
146
+ /** Opens on a mouse coming over the trigger, under `open-on="hover"`. */
147
+ private readonly handleTriggerPointerOver;
148
+ /** Keeps a hover dial open while the mouse is back over it. */
149
+ private readonly handlePointerEnter;
150
+ /**
151
+ * Closes a hover dial a moment after the mouse leaves it, so a mouse
152
+ * crossing the gap between the trigger and the actions keeps it open.
153
+ * Focus inside the dial keeps it open too, as a reader is using it.
154
+ */
155
+ private readonly handlePointerLeave;
156
+ private stay;
131
157
  /**
132
158
  * Puts focus on the trigger, the slotted one or the button drawn in its
133
159
  * place, as a `focus()` on a native control would. The host itself takes no
@@ -31,6 +31,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
31
31
  typeahead: typeaheadByWords(),
32
32
  });
33
33
  this.focusOnOpen = false;
34
+ this.focusLast = false;
34
35
  this.panel = new PopoverController(this, {
35
36
  anchor: () => this.trigger,
36
37
  boundary: () => this,
@@ -47,7 +48,35 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
47
48
  }
48
49
  },
49
50
  });
51
+ this.handleTriggerPointerOver = (event) => {
52
+ if (this.openOn !== 'hover' || event.pointerType !== 'mouse') {
53
+ return;
54
+ }
55
+ this.stay();
56
+ if (!this.open) {
57
+ this.requestOpen(true);
58
+ }
59
+ };
60
+ this.handlePointerEnter = () => {
61
+ this.stay();
62
+ };
63
+ this.handlePointerLeave = (event) => {
64
+ if (this.openOn !== 'hover' || event.pointerType !== 'mouse' || !this.open) {
65
+ return;
66
+ }
67
+ this.stay();
68
+ this.leaving = setTimeout(() => {
69
+ this.leaving = undefined;
70
+ if (this.open && !this.matches(':focus-within')) {
71
+ this.requestOpen(false);
72
+ }
73
+ }, 300);
74
+ };
50
75
  this.handleTriggerClick = () => {
76
+ if (this.open && this.openOn === 'hover') {
77
+ this.items.find((one) => !one.disabled)?.focusItem();
78
+ return;
79
+ }
51
80
  if (this.open) {
52
81
  this.requestOpen(false);
53
82
  return;
@@ -91,11 +120,11 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
91
120
  };
92
121
  this.handleKeydown = (event) => {
93
122
  if (!this.open) {
94
- if (this.pressHeld &&
95
- event.altKey &&
96
- (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
123
+ const arrow = event.key === 'ArrowDown' || event.key === 'ArrowUp';
124
+ if (arrow && (this.pressHeld ? event.altKey : !event.altKey)) {
97
125
  event.preventDefault();
98
126
  this.focusOnOpen = this.requestOpen(true);
127
+ this.focusLast = this.focusOnOpen && event.key === 'ArrowUp';
99
128
  }
100
129
  return;
101
130
  }
@@ -132,6 +161,10 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
132
161
  get pressHeld() {
133
162
  return this.openOn === 'long-press';
134
163
  }
164
+ stay() {
165
+ clearTimeout(this.leaving);
166
+ this.leaving = undefined;
167
+ }
135
168
  focus(options) {
136
169
  const trigger = this.trigger;
137
170
  if (trigger === this) {
@@ -151,6 +184,8 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
151
184
  this.addEventListener('helix-select', this.handleSelect);
152
185
  this.addEventListener('keydown', this.handleKeydown);
153
186
  this.addEventListener('click', this.handleHeldClick, true);
187
+ this.addEventListener('pointerenter', this.handlePointerEnter);
188
+ this.addEventListener('pointerleave', this.handlePointerLeave);
154
189
  }
155
190
  disconnectedCallback() {
156
191
  super.disconnectedCallback();
@@ -162,6 +197,9 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
162
197
  this.removeEventListener('helix-select', this.handleSelect);
163
198
  this.removeEventListener('keydown', this.handleKeydown);
164
199
  this.removeEventListener('click', this.handleHeldClick, true);
200
+ this.removeEventListener('pointerenter', this.handlePointerEnter);
201
+ this.removeEventListener('pointerleave', this.handlePointerLeave);
202
+ this.stay();
165
203
  }
166
204
  announce() {
167
205
  const trigger = this.trigger;
@@ -191,11 +229,17 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
191
229
  super.updated?.(changed);
192
230
  this.announce();
193
231
  this.describeTrigger();
232
+ if (!this.open) {
233
+ this.stay();
234
+ }
194
235
  if (this.open && this.focusOnOpen) {
195
236
  this.focusOnOpen = false;
237
+ const last = this.focusLast;
238
+ this.focusLast = false;
196
239
  this.updateComplete.then(() => {
197
240
  if (this.open) {
198
- this.items.find((one) => !one.disabled)?.focusItem();
241
+ const usable = this.items.filter((one) => !one.disabled);
242
+ (last ? usable.at(-1) : usable[0])?.focusItem();
199
243
  }
200
244
  });
201
245
  }
@@ -209,6 +253,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
209
253
  @focusin=${this.handleTriggerFocusIn}
210
254
  @pointercancel=${this.handleTriggerPointerCancel}
211
255
  @pointerdown=${this.handleTriggerPointerDown}
256
+ @pointerover=${this.handleTriggerPointerOver}
212
257
  @pointermove=${this.handleTriggerPointerMove}
213
258
  @pointerup=${this.handleTriggerPointerUp}
214
259
  @slotchange=${() => this.describeTrigger()}
@@ -222,7 +267,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
222
267
  aria-label=${this.label}
223
268
  >
224
269
  ${''}
225
- ${drawIcon(plus)}
270
+ <slot name="icon">${drawIcon(plus)}</slot>
226
271
  </button>
227
272
  ${held ? html `<span id="long-press" hidden>${this.longPressLabel}</span>` : nothing}
228
273
  </slot>`;
@@ -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) {
@@ -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.