@fusion.dev/helix-headless 0.162.1 → 0.163.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.
@@ -733,6 +733,35 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
733
733
  this.handleSlotChange = () => {
734
734
  this.requestUpdate();
735
735
  };
736
+ this.handleMinimapPointer = (event) => {
737
+ if (event.type === 'pointermove' && (event.buttons & 1) === 0) {
738
+ return;
739
+ }
740
+ const picture = event.currentTarget;
741
+ if (event.type === 'pointerdown') {
742
+ if (event.button !== 0) {
743
+ return;
744
+ }
745
+ capturePointer(picture, event.pointerId);
746
+ }
747
+ event.preventDefault();
748
+ const area = this.minimapArea;
749
+ const shown = picture.getBoundingClientRect();
750
+ const scale = Math.min(shown.width / area.width, shown.height / area.height) || 1;
751
+ const point = {
752
+ x: area.x + (event.clientX - shown.left - (shown.width - area.width * scale) / 2) / scale,
753
+ y: area.y + (event.clientY - shown.top - (shown.height - area.height * scale) / 2) / scale,
754
+ };
755
+ const board = extent(this.nodes);
756
+ const { zoom } = this.at;
757
+ this.direct = event.type === 'pointermove';
758
+ this.touched = true;
759
+ this.at = {
760
+ ...this.at,
761
+ x: this.box.width / 2 - (point.x - board.x + MARGIN) * zoom,
762
+ y: this.box.height / 2 - (point.y - board.y + MARGIN) * zoom,
763
+ };
764
+ };
736
765
  }
737
766
  static { this.words = 'helix-canvas'; }
738
767
  static { this.shadowRootOptions = {
@@ -1120,9 +1149,25 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1120
1149
  if (!from || !to) {
1121
1150
  return undefined;
1122
1151
  }
1123
- const drawn = connector(edge, onPlane(from, origin), onPlane(to, origin), edge.path ?? this.path ?? 'curve', heads(edge));
1152
+ const drawn = this.draw(edge, onPlane(from, origin), onPlane(to, origin), edge.path ?? this.path ?? 'curve', heads(edge));
1124
1153
  return { ...drawn, from, to };
1125
1154
  }
1155
+ draw(edge, from, to, path, drawnHeads) {
1156
+ if (this.edgeRoute) {
1157
+ const { end, fromSide, start, toSide } = ends(edge, from, to);
1158
+ let own;
1159
+ try {
1160
+ own = this.edgeRoute({ edge, end, fromSide, path, start, toSide });
1161
+ }
1162
+ catch (error) {
1163
+ globalThis.reportError?.(error);
1164
+ }
1165
+ if (own?.d) {
1166
+ return { d: own.d, mid: own.mid ?? { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 } };
1167
+ }
1168
+ }
1169
+ return connector(edge, from, to, path, drawnHeads);
1170
+ }
1126
1171
  cardOf(event) {
1127
1172
  return event
1128
1173
  .composedPath()
@@ -2134,7 +2179,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2134
2179
  if (target) {
2135
2180
  const reversed = linking.moving === 'from';
2136
2181
  const [from, to] = reversed ? [target, anchor] : [anchor, target];
2137
- const drawn = connector({
2182
+ const drawn = this.draw({
2138
2183
  fromNode: from.id,
2139
2184
  fromSide: reversed ? linking.toSide : linking.side,
2140
2185
  id: '',
@@ -2157,6 +2202,53 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2157
2202
  }
2158
2203
  return nothing;
2159
2204
  }
2205
+ get onScreen() {
2206
+ const board = extent(this.nodes);
2207
+ const { height, width } = this.box;
2208
+ const { x, y, zoom } = this.at;
2209
+ return {
2210
+ height: height / zoom,
2211
+ width: width / zoom,
2212
+ x: board.x - MARGIN - x / zoom,
2213
+ y: board.y - MARGIN - y / zoom,
2214
+ };
2215
+ }
2216
+ get minimapArea() {
2217
+ const board = extent(this.nodes);
2218
+ const view = this.onScreen;
2219
+ const x = Math.min(board.x, view.x);
2220
+ const y = Math.min(board.y, view.y);
2221
+ const width = Math.max(board.x + board.width, view.x + view.width) - x;
2222
+ const height = Math.max(board.y + board.height, view.y + view.height) - y;
2223
+ const room = Math.max(width, height) * 0.05;
2224
+ return {
2225
+ height: height + room * 2,
2226
+ width: width + room * 2,
2227
+ x: x - room,
2228
+ y: y - room,
2229
+ };
2230
+ }
2231
+ renderMinimap() {
2232
+ const nodes = this.nodes;
2233
+ if (!this.drawsControls('minimap') || nodes.length === 0) {
2234
+ return nothing;
2235
+ }
2236
+ const area = this.minimapArea;
2237
+ const view = this.onScreen;
2238
+ return html `<svg
2239
+ part="minimap"
2240
+ aria-hidden="true"
2241
+ height="120"
2242
+ preserveAspectRatio="xMidYMid meet"
2243
+ viewBox=${`${area.x} ${area.y} ${area.width} ${area.height}`}
2244
+ width="160"
2245
+ @pointerdown=${this.handleMinimapPointer}
2246
+ @pointermove=${this.handleMinimapPointer}
2247
+ >
2248
+ ${nodes.map((node) => svg `<rect part="minimap-node" data-group=${node.type === 'group' ? '' : nothing} x=${node.x} y=${node.y} width=${node.width} height=${node.height} />`)}
2249
+ ${svg `<rect part="minimap-view" x=${view.x} y=${view.y} width=${view.width} height=${view.height} />`}
2250
+ </svg>`;
2251
+ }
2160
2252
  render() {
2161
2253
  const nodes = this.nodes;
2162
2254
  const drawn = extent(nodes);
@@ -2219,7 +2311,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2219
2311
  <slot name="empty"></slot>
2220
2312
  ${this.renderGestures()}
2221
2313
  </div>
2222
- ${this.renderControls()}
2314
+ ${this.renderControls()} ${this.renderMinimap()}
2223
2315
  <span part="live" role="status">${this.said}</span>
2224
2316
  <div part="footer">
2225
2317
  <slot name="footer"></slot>
@@ -2255,6 +2347,9 @@ __decorate([
2255
2347
  words,
2256
2348
  property({ attribute: 'copied-label' })
2257
2349
  ], HelixHeadlessCanvas.prototype, "copiedLabel", void 0);
2350
+ __decorate([
2351
+ property({ attribute: false })
2352
+ ], HelixHeadlessCanvas.prototype, "edgeRoute", void 0);
2258
2353
  __decorate([
2259
2354
  words,
2260
2355
  property({ attribute: 'fallback-label' })
@@ -113,7 +113,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
113
113
  }
114
114
  const invalid = this.touched && !this.internals.validity.valid;
115
115
  this.toggleState('invalid', invalid);
116
- this.internals.ariaInvalid = invalid ? 'true' : null;
116
+ this.internals.ariaInvalid = invalid ? 'true' : 'false';
117
117
  if (invalid !== this.shownInvalid) {
118
118
  this.shownInvalid = invalid;
119
119
  }
@@ -517,12 +517,23 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
517
517
  * Moves focus to turn `at`, drawing it first where the transcript is
518
518
  * windowed and it is not drawn.
519
519
  *
520
- * **A turn takes focus only once a reader moves to it**, so the
521
- * `tabindex="-1"` a turn written as a child needs is written then, long
522
- * after any server's markup has hydrated, and never on a turn nobody moved
523
- * to.
520
+ * **A turn takes focus only once a reader moves to it**, so the `tabindex`
521
+ * it needs is written then, long after any server's markup has hydrated,
522
+ * and never on a turn nobody moved to.
524
523
  */
525
524
  private focusTurn;
525
+ /** The turn that is a tab stop because a reader moved to it. */
526
+ private stop;
527
+ /**
528
+ * Focuses a turn, making it the transcript's one stop a reader moved to.
529
+ *
530
+ * **`0`, and never `-1`.** Chromium leaves the whole of a shadow host at a
531
+ * negative `tabindex` out of the Tab order, its own controls and what is
532
+ * slotted into it alike, so a turn at `-1` lost its links and its Try again
533
+ * once a reader had paged through it. The stop moves with the reader, and a
534
+ * `tabindex` you wrote on a turn is left as it is.
535
+ */
536
+ private visit;
526
537
  /** The first control in the regions `names` holds, in their order. */
527
538
  private firstControlIn;
528
539
  /**
@@ -524,7 +524,9 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
524
524
  }
525
525
  async focusTurn(at) {
526
526
  const count = this.fed ? this.turns.length : this.messages.length;
527
- if (at < count - 1 && this.pinned) {
527
+ const log = this.log;
528
+ const scrolls = log !== null && log !== undefined && log.scrollHeight > log.clientHeight;
529
+ if (at < count - 1 && this.pinned && scrolls) {
528
530
  this.requestPinned(false);
529
531
  }
530
532
  if (this.fed) {
@@ -537,17 +539,27 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
537
539
  this.requestUpdate();
538
540
  await this.updateComplete;
539
541
  }
540
- drawn()?.focus();
542
+ const turn = drawn();
543
+ if (turn) {
544
+ this.visit(turn);
545
+ }
541
546
  return;
542
547
  }
543
548
  const message = this.messages[at];
544
- if (!message) {
545
- return;
549
+ if (message) {
550
+ this.visit(message);
546
551
  }
547
- if (!message.hasAttribute('tabindex')) {
548
- message.setAttribute('tabindex', '-1');
552
+ }
553
+ visit(turn) {
554
+ if (this.stop !== turn) {
555
+ this.stop?.removeAttribute('tabindex');
556
+ this.stop = undefined;
557
+ if (!turn.hasAttribute('tabindex')) {
558
+ turn.setAttribute('tabindex', '0');
559
+ this.stop = turn;
560
+ }
549
561
  }
550
- message.focus();
562
+ turn.focus();
551
563
  }
552
564
  firstControlIn(names) {
553
565
  for (const name of names) {
@@ -582,7 +594,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
582
594
  .turnWords=${this.words}
583
595
  .turnPosition=${first + offset + 1}
584
596
  .turnCount=${this.turns.length}
585
- tabindex="-1"
586
597
  .failedLabel=${this.failedLabel}
587
598
  state=${turn.state ?? nothing}
588
599
  >${turn.text}</${message}>
@@ -46,7 +46,8 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
46
46
  * same `group`, such as a route and its dotted legs, share one tile. A tile
47
47
  * sends `helix-before-toggle` from the layer first, so you can store the
48
48
  * choice or refuse it, and `helix-toggle` from it once `hidden` holds the
49
- * choice.
49
+ * choice. Escape closes the menu and returns focus to its button, and the menu
50
+ * closes when focus moves on from it or a press lands outside the map.
50
51
  *
51
52
  * Imagery, a `helix-map-raster` with `ground`, is a map type and not a tile.
52
53
  * The menu opens with a card for each type, Standard, Hybrid, and Satellite,
@@ -315,6 +316,12 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
315
316
  private hiding?;
316
317
  /** Closes the layers menu on a press anywhere outside the frame. */
317
318
  private readonly handleOutside;
319
+ /**
320
+ * Closes the layers menu when focus moves on from its button and the menu,
321
+ * as a Tab past the last tile does. Focus leaving the page, with nothing
322
+ * focused in its place, leaves the menu as it is.
323
+ */
324
+ private readonly handleMenuFocusOut;
318
325
  /**
319
326
  * Keys pressed in the layers menu. Escape closes it and returns to its
320
327
  * button, and no key reaches the region, where the arrows would pan the map.
@@ -82,6 +82,16 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
82
82
  this.layersOpen = false;
83
83
  }
84
84
  };
85
+ this.handleMenuFocusOut = (event) => {
86
+ const to = event.relatedTarget;
87
+ if (!(this.layersOpen && to)) {
88
+ return;
89
+ }
90
+ const owners = this.renderRoot.querySelectorAll('[part="layers-toggle"], [part="layers-menu"]');
91
+ if (![...owners].some((owner) => owner.contains(to))) {
92
+ this.layersOpen = false;
93
+ }
94
+ };
85
95
  this.handleMenuKeydown = (event) => {
86
96
  event.stopPropagation();
87
97
  if (event.key === 'Escape' && this.layersOpen) {
@@ -402,6 +412,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
402
412
  this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'menu' }));
403
413
  }
404
414
  }}
415
+ @focusout=${this.handleMenuFocusOut}
405
416
  @keydown=${this.handleMenuKeydown}
406
417
  >
407
418
  <slot name="layers-icon">
@@ -416,6 +427,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
416
427
  role="group"
417
428
  aria-label=${this.layersLabel}
418
429
  ?hidden=${!this.layersOpen}
430
+ @focusout=${this.handleMenuFocusOut}
419
431
  @keydown=${this.handleMenuKeydown}
420
432
  >
421
433
  ${this.imagery.length > 0 ? this.renderMapTypes() : nothing}
@@ -10,16 +10,22 @@ import '../../kbd/index.js';
10
10
  * - `checked` makes the row a choice.
11
11
  * - With neither, the row is a command.
12
12
  *
13
- * The role follows the kind. A row with `checked` is `menuitemcheckbox` with
14
- * `aria-checked`, and `exclusive` makes it `menuitemradio`, for a set where
15
- * only one can be on. A row that opens a page keeps `menuitem` and sets
16
- * `aria-haspopup`.
13
+ * The role follows the kind. A row with `checkable` or `checked` is
14
+ * `menuitemcheckbox` with `aria-checked`, and an `exclusive` row is
15
+ * `menuitemradio`, for a set where only one can be on. A row that opens a page
16
+ * keeps `menuitem` and sets `aria-haspopup`.
17
17
  *
18
18
  * Choosing a choice row checks or unchecks it before `helix-select` is sent,
19
19
  * so a listener reads the new `checked`. An `exclusive` row turns on and turns
20
20
  * off the `exclusive` rows next to it. Cancel `helix-before-toggle` to keep a
21
21
  * row as it is and set `checked` yourself.
22
22
  *
23
+ * Space on a choice row changes it and leaves the menu open, so a reader can
24
+ * set several in a row, as the APG's menu does. Enter and a press change it and
25
+ * close the menu. `close-on-select="false"` on the row keeps the menu open after
26
+ * any choice of it, and `close-on-select` on the row closes it after a Space as
27
+ * well.
28
+ *
23
29
  * The menu sets whether the row's page is showing as a property, never as an
24
30
  * attribute. An attribute written onto your element after it upgrades causes a
25
31
  * hydration mismatch.
@@ -82,7 +88,8 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
82
88
  * as a checkbox tells a reader its siblings are commands, which is the one
83
89
  * thing they are not.
84
90
  *
85
- * `checked` implies it, so a row that is on never has to say both.
91
+ * `checked` and `exclusive` imply it, so a row that is on, or one of a set,
92
+ * never has to say both.
86
93
  *
87
94
  * Choosing the row checks or unchecks it, after a `helix-before-toggle` that
88
95
  * you can cancel.
@@ -95,6 +102,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
95
102
  * state you start the row in, and `:state(checked)` carries the current one.
96
103
  */
97
104
  checked: boolean;
105
+ /**
106
+ * Whether choosing the row closes the menu.
107
+ *
108
+ * Unset takes the menu's `close-on-select`, which is on, and keeps the menu
109
+ * open after a Space on a choice row. Write `close-on-select="false"` for a
110
+ * row that changes something the reader is watching, such as a zoom step.
111
+ */
112
+ closeOnSelect?: boolean;
98
113
  /**
99
114
  * Whether the row is disabled, so it cannot be chosen.
100
115
  */
@@ -116,8 +131,7 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
116
131
  * it checked. `menuitemradio` tells a reader which kind of set they are in,
117
132
  * where `menuitemcheckbox` says every row is independent.
118
133
  *
119
- * Meaningless without `checkable`, since a row that cannot be ticked is not
120
- * one of a set of anything.
134
+ * It makes the row a choice, so it needs no `checkable` beside it.
121
135
  */
122
136
  exclusive: boolean;
123
137
  /**
@@ -192,6 +206,17 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
192
206
  * protected. Not a consumer's to read or set.
193
207
  */
194
208
  readonly isMenuItem = true;
209
+ /**
210
+ * @internal Whether the last choice of this row asked the menu to stay open:
211
+ * a Space on a choice row, or `close-on-select="false"`. Read by the menu
212
+ * while it hears the row's `helix-select`.
213
+ */
214
+ keepsOpen: boolean;
215
+ /**
216
+ * @internal Whether the row is a choice: it can be ticked, or is one of a
217
+ * set. Read by the menu to reserve the trailing column.
218
+ */
219
+ get choice(): boolean;
195
220
  /**
196
221
  * Whether anything was slotted into `submenu`.
197
222
  *
@@ -342,8 +367,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
342
367
  private tick;
343
368
  /** The other `exclusive` rows in this row's run of siblings. */
344
369
  private exclusivePeers;
370
+ /**
371
+ * Chooses the row. `space` is whether a Space chose it, which keeps the menu
372
+ * open on a choice row unless the row says otherwise.
373
+ */
345
374
  private choose;
346
375
  private readonly handleClick;
376
+ /** Whether the click on its way is the one a Space on a linked row sent. */
377
+ private spacePressed;
347
378
  /**
348
379
  * Enter and Space choose the row, which a `<div>` would not do on its own.
349
380
  *
@@ -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 { ariaChord, auditName, customElement, deepActiveElement, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, isRtl, onApple, PopoverController, requestChange, slotFilled, } from '@fusion.dev/helix-core';
7
+ import { ariaChord, auditName, customElement, deepActiveElement, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, isRtl, onApple, onUnlessFalse, PopoverController, requestChange, slotFilled, } from '@fusion.dev/helix-core';
8
8
  import { caretRight, check } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -29,6 +29,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
29
29
  this.target = '';
30
30
  this.value = '';
31
31
  this.isMenuItem = true;
32
+ this.keepsOpen = false;
32
33
  this.submenu = false;
33
34
  this.start = false;
34
35
  this.end = false;
@@ -58,8 +59,10 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
58
59
  surface: () => (this.flyout ? this.partNamed('surface') : null),
59
60
  });
60
61
  this.handleClick = () => {
61
- this.choose();
62
+ this.choose(this.spacePressed);
63
+ this.spacePressed = false;
62
64
  };
65
+ this.spacePressed = false;
63
66
  this.handleKeydown = (event) => {
64
67
  if (event.key !== 'Enter' && event.key !== ' ') {
65
68
  return;
@@ -70,12 +73,17 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
70
73
  }
71
74
  event.preventDefault();
72
75
  if (link) {
76
+ this.spacePressed = true;
73
77
  link.click();
78
+ this.spacePressed = false;
74
79
  return;
75
80
  }
76
- this.handleClick();
81
+ this.choose(event.key === ' ');
77
82
  };
78
83
  }
84
+ get choice() {
85
+ return this.checkable || this.checked || this.exclusive;
86
+ }
79
87
  get hasSubmenu() {
80
88
  return this.submenu;
81
89
  }
@@ -93,9 +101,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
93
101
  const rows = this.knownChildren.filter((child) => child.slot === 'submenu' && child.isMenuItem);
94
102
  this.toggleState('submenu-start', this.flyout && rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
95
103
  this.toggleState('submenu-mark', this.flyout &&
96
- rows.some((row) => row.checkable ||
97
- row.checked ||
98
- [...row.children].some((child) => child.slot === 'submenu')));
104
+ rows.some((row) => row.choice || [...row.children].some((child) => child.slot === 'submenu')));
99
105
  }
100
106
  firstUpdated(changed) {
101
107
  super.firstUpdated(changed);
@@ -152,7 +158,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
152
158
  return this.active;
153
159
  }
154
160
  tick() {
155
- if (!(this.checkable || this.checked) || this.hasSubmenu) {
161
+ if (!this.choice || this.hasSubmenu) {
156
162
  return;
157
163
  }
158
164
  const next = this.exclusive ? true : !this.checked;
@@ -188,10 +194,12 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
188
194
  }
189
195
  return peers;
190
196
  }
191
- choose() {
197
+ choose(space = false) {
192
198
  if (this.disabled) {
193
199
  return;
194
200
  }
201
+ const spaceKeeps = space && this.choice && !this.hasSubmenu;
202
+ this.keepsOpen = this.closeOnSelect === undefined ? spaceKeeps : !this.closeOnSelect;
195
203
  this.tick();
196
204
  this.dispatchEvent(new HelixSelectEvent({ value: this.value }));
197
205
  }
@@ -221,7 +229,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
221
229
  }
222
230
  const submenu = this.hasSubmenu;
223
231
  const linked = this.href !== '' && !this.disabled;
224
- const ticks = this.checkable || this.checked;
232
+ const ticks = this.choice;
225
233
  const role = ticks ? (this.exclusive ? 'menuitemradio' : 'menuitemcheckbox') : 'menuitem';
226
234
  const checkedState = ticks ? (this.checked ? 'true' : 'false') : nothing;
227
235
  const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
@@ -281,6 +289,9 @@ __decorate([
281
289
  __decorate([
282
290
  property({ type: Boolean })
283
291
  ], HelixHeadlessMenuItem.prototype, "checked", void 0);
292
+ __decorate([
293
+ property({ attribute: 'close-on-select', converter: onUnlessFalse })
294
+ ], HelixHeadlessMenuItem.prototype, "closeOnSelect", void 0);
284
295
  __decorate([
285
296
  property({ reflect: true, type: Boolean })
286
297
  ], HelixHeadlessMenuItem.prototype, "disabled", void 0);
@@ -40,7 +40,8 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
40
40
  * Keys: Enter, Space or Down Arrow on the trigger opens the menu on its first
41
41
  * row, and Up Arrow on its last. In the menu, Up and Down Arrow move between
42
42
  * rows, Home and End go to the first and the last, a typed letter moves to
43
- * the next row whose name starts with it, Enter and Space choose, Right Arrow
43
+ * the next row whose name starts with it, Enter and Space choose (Space on a
44
+ * choice row changes it and leaves the menu open), Right Arrow
44
45
  * opens a row's submenu and Left Arrow goes back, the two swapping on a
45
46
  * right-to-left page. Left Arrow on the top page does nothing. Escape closes
46
47
  * the menu and returns focus to the trigger. Tab closes it and moves on.
@@ -109,6 +110,14 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
109
110
  * The text of the row that returns from a submenu.
110
111
  */
111
112
  backLabel: string;
113
+ /**
114
+ * Whether choosing a row closes the menu.
115
+ *
116
+ * On by default, and a Space on a choice row leaves the menu open either way.
117
+ * Write `close-on-select="false"` to keep the menu open after every choice, as
118
+ * Ark UI's `closeOnSelect` does. A row's own `close-on-select` wins over this.
119
+ */
120
+ closeOnSelect: boolean;
112
121
  /**
113
122
  * Whether the menu is the trigger's context menu: it opens where the reader
114
123
  * asks for one, with a right-click, a long press, Shift+F10 or the menu key.
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -31,6 +31,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
31
31
  constructor() {
32
32
  super(...arguments);
33
33
  this.backLabel = 'Back';
34
+ this.closeOnSelect = true;
34
35
  this.context = false;
35
36
  this.label = '';
36
37
  this.longPressLabel = 'Long press or press Alt+Down to show the menu';
@@ -119,8 +120,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
119
120
  this.enter(chosen);
120
121
  return;
121
122
  }
123
+ const keep = chosen.keepsOpen || (chosen.closeOnSelect === undefined && !this.closeOnSelect);
122
124
  queueMicrotask(() => {
123
- if (!event.defaultPrevented) {
125
+ if (!(event.defaultPrevented || keep)) {
124
126
  this.hide();
125
127
  }
126
128
  });
@@ -538,7 +540,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
538
540
  markColumns() {
539
541
  const rows = this.flies ? this.topRows : this.drilldown.rows;
540
542
  this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
541
- this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
543
+ this.toggleState('mark', rows.some((row) => row.choice || this.drilldown.hasPage(row)));
542
544
  }
543
545
  mark(item, onPage) {
544
546
  const page = this.drilldown.page;
@@ -608,6 +610,9 @@ __decorate([
608
610
  words,
609
611
  property({ attribute: 'back-label' })
610
612
  ], HelixHeadlessMenu.prototype, "backLabel", void 0);
613
+ __decorate([
614
+ property({ attribute: 'close-on-select', converter: onUnlessFalse })
615
+ ], HelixHeadlessMenu.prototype, "closeOnSelect", void 0);
611
616
  __decorate([
612
617
  property({ reflect: true, type: Boolean })
613
618
  ], HelixHeadlessMenu.prototype, "context", void 0);
@@ -150,7 +150,13 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
150
150
  protected readonly keys: KeyboardNavigationController;
151
151
  /** The steps, in the order the consumer wrote them. */
152
152
  private get steps();
153
- /** The index of the stage the reader is on, or `-1`. */
153
+ /**
154
+ * The index of the stage the reader is on, or `-1`.
155
+ *
156
+ * Read from `active` once anything has set it, because a move writes
157
+ * `active` at once and each step's `current` only on the next update: two
158
+ * calls to `next()` in one task then move two stages.
159
+ */
154
160
  private get at();
155
161
  /**
156
162
  * Moves to the next stage that can be gone to, as Ark UI's `goToNextStep`
@@ -58,7 +58,12 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
58
58
  return [...this.querySelectorAll(':scope > *')].filter((one) => 'current' in one);
59
59
  }
60
60
  get at() {
61
- return this.steps.findIndex((step) => step.current);
61
+ const steps = this.steps;
62
+ if (this.active === '') {
63
+ return steps.findIndex((step) => step.current);
64
+ }
65
+ const item = this.resolveActive(steps);
66
+ return item ? steps.indexOf(item) : -1;
62
67
  }
63
68
  next() {
64
69
  return this.step(1);