@fusion.dev/helix-headless 0.162.0 → 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.
Files changed (44) hide show
  1. package/custom-elements.json +342 -15
  2. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +3 -1
  3. package/dist/breadcrumbs/root/breadcrumbs-root.js +2 -1
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/canvas/root/canvas-document.d.ts +33 -0
  6. package/dist/canvas/root/canvas-root.d.ts +50 -2
  7. package/dist/canvas/root/canvas-root.js +98 -3
  8. package/dist/chat/composer/chat-composer.js +1 -1
  9. package/dist/chat/root/chat-root.d.ts +21 -5
  10. package/dist/chat/root/chat-root.js +21 -9
  11. package/dist/checkbox-group/root/checkbox-group-root.d.ts +3 -1
  12. package/dist/checkbox-group/root/checkbox-group-root.js +2 -1
  13. package/dist/combobox/root/combobox-root.d.ts +8 -1
  14. package/dist/combobox/root/combobox-root.js +5 -1
  15. package/dist/list/root/list-root.d.ts +3 -1
  16. package/dist/list/root/list-root.js +2 -1
  17. package/dist/listbox/root/listbox-root.d.ts +2 -1
  18. package/dist/listbox/root/listbox-root.js +2 -1
  19. package/dist/map/root/map-root.d.ts +8 -1
  20. package/dist/map/root/map-root.js +12 -0
  21. package/dist/menu/item/menu-item.d.ts +38 -7
  22. package/dist/menu/item/menu-item.js +20 -9
  23. package/dist/menu/root/menu-root.d.ts +10 -1
  24. package/dist/menu/root/menu-root.js +8 -3
  25. package/dist/notifications/group/notification-group.d.ts +4 -1
  26. package/dist/notifications/group/notification-group.js +5 -3
  27. package/dist/notifications/root/notifications-root.d.ts +7 -2
  28. package/dist/notifications/root/notifications-root.js +2 -1
  29. package/dist/radio/group/radio-group.d.ts +2 -1
  30. package/dist/radio/group/radio-group.js +2 -1
  31. package/dist/select/root/select-root.d.ts +8 -1
  32. package/dist/select/root/select-root.js +5 -1
  33. package/dist/steps/root/steps-root.d.ts +7 -1
  34. package/dist/steps/root/steps-root.js +6 -1
  35. package/dist/tag/group/tag-group.d.ts +3 -1
  36. package/dist/tag/group/tag-group.js +2 -1
  37. package/dist/toggle/group/toggle-group.d.ts +2 -1
  38. package/dist/toggle/group/toggle-group.js +4 -1
  39. package/dist/tree/root/tree-root.d.ts +2 -1
  40. package/dist/tree/root/tree-root.js +4 -1
  41. package/lint.json +1 -1
  42. package/package.json +5 -5
  43. package/types/jsx.d.ts +162 -24
  44. package/types/vue.d.ts +40 -7
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  /** One step of a trail written as data, for `items`. */
4
4
  export interface BreadcrumbItem {
@@ -120,6 +120,8 @@ export declare class HelixHeadlessBreadcrumbs extends HelixElement {
120
120
  private readonly markLast;
121
121
  /** New crumbs fold again, as a new `items` does. */
122
122
  private readonly handleSlotChange;
123
+ /** Reads a crumb again once it upgrades, so the current one is marked. */
124
+ protected readonly upgradeWatch: UpgradeController;
123
125
  /** The crumbs `items` describes, each a crumb of this tier. */
124
126
  protected renderItems(): TemplateResult | typeof nothing;
125
127
  protected render(): TemplateResult;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, formatWords, HelixElement, hostAria, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, formatWords, HelixElement, hostAria, UpgradeController, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { repeat } from 'lit/directives/repeat.js';
@@ -55,6 +55,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
55
55
  this.expanded = false;
56
56
  this.markLast();
57
57
  };
58
+ this.upgradeWatch = new UpgradeController(this, this.handleSlotChange);
58
59
  }
59
60
  static { this.words = 'helix-breadcrumbs'; }
60
61
  get slotted() {
@@ -1,4 +1,4 @@
1
1
  export { HelixHeadlessCanvasEdge } from './edge/canvas-edge.js';
2
2
  export { HelixHeadlessCanvasNode } from './node/canvas-node.js';
3
- export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasSide, CanvasTone, } from './root/canvas-root.js';
3
+ export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from './root/canvas-root.js';
4
4
  export { fromJsonCanvas, HelixHeadlessCanvas } from './root/canvas-root.js';
@@ -91,6 +91,39 @@ export declare const port: (node: CanvasNode, side: CanvasSide) => {
91
91
  };
92
92
  /** How a connector travels between the two cards it joins. */
93
93
  export type CanvasPath = 'curve' | 'straight' | 'step';
94
+ /** What a board's `edgeRoute` is given for one connection, in the board's own units. */
95
+ export interface CanvasRouteEnds {
96
+ /** The connection. */
97
+ edge: CanvasEdge;
98
+ /** Where it arrives, on the side of the card it arrives at. */
99
+ end: {
100
+ x: number;
101
+ y: number;
102
+ };
103
+ /** The side of the card it leaves. */
104
+ fromSide: CanvasSide;
105
+ /** The route the board would draw: the connection's `path`, or the board's. */
106
+ path: CanvasPath;
107
+ /** Where it leaves, on the side of the card it leaves. */
108
+ start: {
109
+ x: number;
110
+ y: number;
111
+ };
112
+ /** The side of the card it arrives at. */
113
+ toSide: CanvasSide;
114
+ }
115
+ /**
116
+ * Draws a connection's line: its SVG path from `start` to `end`, and where its
117
+ * label sits, the middle of the two ends where it says nothing. Nothing back
118
+ * draws the board's own.
119
+ */
120
+ export type CanvasRoute = (ends: CanvasRouteEnds) => {
121
+ d: string;
122
+ mid?: {
123
+ x: number;
124
+ y: number;
125
+ } | undefined;
126
+ } | undefined;
94
127
  /** A point in the document's own units. */
95
128
  type Point = {
96
129
  x: number;
@@ -1,7 +1,7 @@
1
1
  import { type CanvasView, HelixElement, type ViewportPanDetail, type ViewportWay, type ViewportZoomDetail } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import { type CanvasDocument, type CanvasTone } from './canvas-document.js';
4
- export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasSide, CanvasTone, } from './canvas-document.js';
3
+ import { type CanvasDocument, type CanvasRoute, type CanvasTone } from './canvas-document.js';
4
+ export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from './canvas-document.js';
5
5
  export { fromJsonCanvas } from './canvas-document.js';
6
6
  declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion.dev/helix-core").ViewportElement) & typeof HelixElement;
7
7
  /**
@@ -92,6 +92,17 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
92
92
  * connection is a new element, and React discards an element the board adds to
93
93
  * its markup on the next render, so you write the connection.
94
94
  *
95
+ * **`edgeRoute` draws the connections' lines your own way**: a function from
96
+ * where a line leaves and arrives, and the sides it does, to its SVG path, for
97
+ * a route such as one that bends around the cards between. What it returns
98
+ * is the line drawn, the line pressed and focused, and the one an arrowhead
99
+ * sits at the end of. Nothing back draws the board's own.
100
+ *
101
+ * Add `minimap` to `controls` for a picture of the whole board in a corner:
102
+ * each card as a box, and a frame around what is on screen. A press or a drag
103
+ * on it brings that place to the middle of the view. It is for a pointer, and
104
+ * a screen reader skips it: the arrow keys and `show()` reach every card.
105
+ *
95
106
  * Cards and connections are `helix-canvas-node` and `helix-canvas-edge`
96
107
  * elements, so they are in the served markup whichever renderer wrote them. The
97
108
  * library fetches nothing, and an array passed to an element is missing from
@@ -148,6 +159,9 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
148
159
  * @csspart label-editor - The field for editing a connection's label or a group's name, placed over that text.
149
160
  * @csspart live - A live region that screen readers announce while a connection is being made.
150
161
  * @csspart marquee - The box a Shift-drag across the empty board draws, which selects the cards it touches.
162
+ * @csspart minimap - The picture of the whole board, under `controls="minimap"`.
163
+ * @csspart minimap-node - One card in the minimap.
164
+ * @csspart minimap-view - The frame in the minimap around what is on screen.
151
165
  * @csspart pan-down - The button that moves the board down.
152
166
  * @csspart pan-end - The button that moves the board toward the end of a line of text: right in English.
153
167
  * @csspart pan-start - The button that moves the board toward the start of a line of text: left in English.
@@ -210,6 +224,15 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
210
224
  controlsLabel: string;
211
225
  /** What screen readers hear after a copy, with `{count}` for the cards copied. */
212
226
  copiedLabel: string;
227
+ /**
228
+ * Draws every connection's line: a function from where it leaves and
229
+ * arrives, and the sides it does, to its SVG path and where its label sits.
230
+ *
231
+ * Absent, or returning nothing for a connection, draws the board's own
232
+ * route. A function that throws draws the board's own too, and the error is
233
+ * reported to the page as an uncaught one. Set it as a property.
234
+ */
235
+ edgeRoute?: CanvasRoute;
213
236
  /** The board's name where `label` is empty. */
214
237
  fallbackLabel: string;
215
238
  /**
@@ -702,6 +725,8 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
702
725
  * both its cards are on the board; nothing where either is not.
703
726
  */
704
727
  private geometryOf;
728
+ /** One connection's line, by `edgeRoute` where it draws one, and by the board's own where not. */
729
+ private draw;
705
730
  /** The card an event came from, wherever inside it the event began. */
706
731
  private cardOf;
707
732
  /**
@@ -1244,6 +1269,29 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
1244
1269
  * the card a keyboard reader is on.
1245
1270
  */
1246
1271
  private renderDraft;
1272
+ /** The part of the document on screen, in its own units. */
1273
+ private get onScreen();
1274
+ /** What the minimap shows: every card and what is on screen, with room around them. */
1275
+ private get minimapArea();
1276
+ /**
1277
+ * A press or a drag on the minimap, which brings the place under it to the
1278
+ * middle of the view.
1279
+ *
1280
+ * **The point is read through the picture's own scale**: the area is drawn
1281
+ * whole and centered in the box, so the box's spare width or height is split
1282
+ * either side of it.
1283
+ */
1284
+ private readonly handleMinimapPointer;
1285
+ /**
1286
+ * The minimap, under `controls="minimap"`: each card as a box, and a frame
1287
+ * around what is on screen.
1288
+ *
1289
+ * **A picture for a pointer**, hidden from screen readers and taking no
1290
+ * focus, as a map's own overview is: the arrow keys and `show()` already take
1291
+ * a keyboard to any card, and a second way that says nothing a reader can
1292
+ * use would be a stop that does nothing.
1293
+ */
1294
+ private renderMinimap;
1247
1295
  protected render(): TemplateResult;
1248
1296
  }
1249
1297
  declare global {
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { ChildrenController, HelixElement } from '@fusion.dev/helix-core';
1
+ import { ChildrenController, HelixElement, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { HelixHeadlessChatMessage } from '../message/chat-message.js';
4
4
  /** One turn handed over as data, which is what `turns` holds. */
@@ -308,6 +308,11 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
308
308
  * never a turn's own text, which a streaming turn rewrites every frame.
309
309
  */
310
310
  protected readonly childWatch: ChildrenController;
311
+ /**
312
+ * Hears a message upgrade after it arrived. A message is known by its marker,
313
+ * which an element that has not upgraded lacks, so it was assigned nowhere.
314
+ */
315
+ protected readonly upgradeWatch: UpgradeController;
311
316
  /** Watches the turns' own `state`, which is when one of them settles. */
312
317
  private settling;
313
318
  /** Watches the turns and the log for size, which scrolling does not report. */
@@ -512,12 +517,23 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
512
517
  * Moves focus to turn `at`, drawing it first where the transcript is
513
518
  * windowed and it is not drawn.
514
519
  *
515
- * **A turn takes focus only once a reader moves to it**, so the
516
- * `tabindex="-1"` a turn written as a child needs is written then, long
517
- * after any server's markup has hydrated, and never on a turn nobody moved
518
- * 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.
519
523
  */
520
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;
521
537
  /** The first control in the regions `names` holds, in their order. */
522
538
  private firstControlIn;
523
539
  /**
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessChat_1;
8
- import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
8
+ import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
9
9
  import { arrowDown } from '@fusion.dev/helix-icons';
10
10
  import { nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
@@ -63,6 +63,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
63
63
  this.said = 0;
64
64
  this.words = TURN_WORDS;
65
65
  this.childWatch = new ChildrenController(this, () => this.handleSlotChange());
66
+ this.upgradeWatch = new UpgradeController(this, () => this.handleSlotChange());
66
67
  this.settling = undefined;
67
68
  this.grower = undefined;
68
69
  this.watched = [];
@@ -523,7 +524,9 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
523
524
  }
524
525
  async focusTurn(at) {
525
526
  const count = this.fed ? this.turns.length : this.messages.length;
526
- 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) {
527
530
  this.requestPinned(false);
528
531
  }
529
532
  if (this.fed) {
@@ -536,17 +539,27 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
536
539
  this.requestUpdate();
537
540
  await this.updateComplete;
538
541
  }
539
- drawn()?.focus();
542
+ const turn = drawn();
543
+ if (turn) {
544
+ this.visit(turn);
545
+ }
540
546
  return;
541
547
  }
542
548
  const message = this.messages[at];
543
- if (!message) {
544
- return;
549
+ if (message) {
550
+ this.visit(message);
545
551
  }
546
- if (!message.hasAttribute('tabindex')) {
547
- 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
+ }
548
561
  }
549
- message.focus();
562
+ turn.focus();
550
563
  }
551
564
  firstControlIn(names) {
552
565
  for (const name of names) {
@@ -581,7 +594,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
581
594
  .turnWords=${this.words}
582
595
  .turnPosition=${first + offset + 1}
583
596
  .turnCount=${this.turns.length}
584
- tabindex="-1"
585
597
  .failedLabel=${this.failedLabel}
586
598
  state=${turn.state ?? nothing}
587
599
  >${turn.text}</${message}>
@@ -1,4 +1,4 @@
1
- import { DisabledCascadeController, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { DisabledCascadeController, HelixFormElement, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { HelixHeadlessCheckbox } from '../../checkbox/root/checkbox-root.js';
4
4
  import '../../checkbox/index.js';
@@ -132,6 +132,8 @@ export declare class HelixHeadlessCheckboxGroup extends HelixHeadlessCheckboxGro
132
132
  private pending;
133
133
  /** Watches for boxes arriving inside a wrapper, which no slot reports. */
134
134
  private observer;
135
+ /** Reads a box again once it upgrades, so it takes the group's name and value. */
136
+ protected readonly upgradeWatch: UpgradeController;
135
137
  /** The value the required check last judged. */
136
138
  private judged;
137
139
  /** The boxes `member` is added to. */
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessCheckboxGroup_1;
8
- import { commaList, customElement, DisabledCascadeController, HelixBeforeChangeEvent, HelixFormElement, hostAria, MixinField, words, } from '@fusion.dev/helix-core';
8
+ import { commaList, customElement, DisabledCascadeController, HelixBeforeChangeEvent, HelixFormElement, hostAria, MixinField, UpgradeController, words, } from '@fusion.dev/helix-core';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { html } from 'lit/static-html.js';
@@ -21,6 +21,7 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
21
21
  this.declared = [];
22
22
  this.checking = undefined;
23
23
  this.pending = false;
24
+ this.upgradeWatch = new UpgradeController(this, undefined, { subtree: true });
24
25
  this.watched = new Set();
25
26
  this.member = {
26
27
  hostUpdated: () => {
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
4
  import '../../tag/index.js';
@@ -376,6 +376,13 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
376
376
  */
377
377
  get type(): string;
378
378
  private get listbox();
379
+ /**
380
+ * Hears an option upgrade after it arrived. The options are this element's
381
+ * children, which the listbox reaches through a slot and does not watch, so
382
+ * both read them again: the listbox to select the one `value` names, and this
383
+ * element for the words in its field.
384
+ */
385
+ protected readonly upgradeWatch: UpgradeController;
379
386
  protected readonly panel: PopoverController;
380
387
  /** The values chosen, in the order they were chosen. */
381
388
  private get values();
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, afterLinkFollowed, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -50,6 +50,10 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
50
50
  this.emptied = false;
51
51
  this.opening = undefined;
52
52
  this.watcher = undefined;
53
+ this.upgradeWatch = new UpgradeController(this, () => {
54
+ this.listbox?.requestUpdate();
55
+ this.requestUpdate();
56
+ }, { subtree: true });
53
57
  this.panel = new PopoverController(this, {
54
58
  anchor: () => this.partNamed('field'),
55
59
  boundary: () => [
@@ -1,4 +1,4 @@
1
- import { HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
1
+ import { HelixElement, KeyboardNavigationController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { HelixHeadlessListItem } from '../item/list-item.js';
4
4
  import { type ListSelectionBehavior, type ListSelectionMode } from '../selection.js';
@@ -405,6 +405,8 @@ export declare class HelixHeadlessList extends HelixElement {
405
405
  private askForMore;
406
406
  private readonly handleEmptyChange;
407
407
  private readonly handleSlotChange;
408
+ /** Reads an item again once it upgrades, so it takes the role the list gives its rows. */
409
+ protected readonly upgradeWatch: UpgradeController;
408
410
  }
409
411
  declare global {
410
412
  interface HTMLElementTagNameMap {
@@ -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 { auditName, commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, HelixLoadMoreEvent, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { auditName, commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, HelixLoadMoreEvent, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, UpgradeController, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessListItem } from '../item/list-item.js';
@@ -138,6 +138,7 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
138
138
  this.rowsChanged = true;
139
139
  this.requestUpdate();
140
140
  };
141
+ this.upgradeWatch = new UpgradeController(this, this.handleSlotChange);
141
142
  }
142
143
  static { this.words = 'helix-list'; }
143
144
  blocked(row) {
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, type DataOptionItem, DisabledCascadeController, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, type DataOptionItem, DisabledCascadeController, HelixFormElement, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
@@ -436,6 +436,7 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
436
436
  * exactly as it moves through a flat one.
437
437
  */
438
438
  get options(): HelixHeadlessOption[];
439
+ protected readonly upgradeWatch: UpgradeController;
439
440
  /**
440
441
  * The elements of one kind in the list: its options, its groups or its
441
442
  * rules. Walks the tree, and walks *through* a slot when it meets one.
@@ -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, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -51,6 +51,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
51
51
  items: () => this.options,
52
52
  });
53
53
  this.chosenAgain = undefined;
54
+ this.upgradeWatch = new UpgradeController(this, undefined, { subtree: true });
54
55
  this.handleSelection = (event) => {
55
56
  if (event.detail.href !== undefined) {
56
57
  return;
@@ -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}