@fusion.dev/helix-headless 0.176.1 → 0.177.1

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 (51) hide show
  1. package/custom-elements.json +1383 -766
  2. package/dist/area-chart/root/area-chart-root.d.ts +18 -1
  3. package/dist/area-chart/root/area-chart-root.js +2 -2
  4. package/dist/avatar/root/avatar-root.d.ts +1 -1
  5. package/dist/avatar/root/avatar-root.js +1 -1
  6. package/dist/bar-chart/root/bar-chart-root.d.ts +19 -1
  7. package/dist/bar-chart/root/bar-chart-root.js +2 -2
  8. package/dist/button/root/button-root.js +1 -0
  9. package/dist/canvas/node/canvas-node.js +1 -1
  10. package/dist/canvas/root/canvas-root.d.ts +5 -1
  11. package/dist/canvas/root/canvas-root.js +9 -1
  12. package/dist/chat/composer/chat-composer.d.ts +1 -1
  13. package/dist/chat/composer/chat-composer.js +1 -1
  14. package/dist/color-picker/root/color-picker-root.d.ts +10 -0
  15. package/dist/color-picker/root/color-picker-root.js +20 -13
  16. package/dist/combobox/root/combobox-root.d.ts +13 -0
  17. package/dist/combobox/root/combobox-root.js +16 -4
  18. package/dist/donut-chart/root/donut-chart-root.d.ts +37 -2
  19. package/dist/donut-chart/root/donut-chart-root.js +22 -8
  20. package/dist/element-events.generated.js +2 -0
  21. package/dist/file-input/root/file-input-root.d.ts +3 -2
  22. package/dist/file-input/root/file-input-root.js +5 -2
  23. package/dist/graph/root/graph-root.d.ts +28 -3
  24. package/dist/graph/root/graph-root.js +38 -3
  25. package/dist/heatmap/root/heatmap-root.d.ts +28 -5
  26. package/dist/heatmap/root/heatmap-root.js +7 -2
  27. package/dist/line-chart/root/line-chart-root.d.ts +18 -1
  28. package/dist/line-chart/root/line-chart-root.js +2 -2
  29. package/dist/loader/root/loader-root.d.ts +15 -0
  30. package/dist/loader/root/loader-root.js +28 -6
  31. package/dist/pin-input/root/pin-input-root.js +1 -0
  32. package/dist/rating/root/rating-root.d.ts +2 -1
  33. package/dist/rating/root/rating-root.js +8 -2
  34. package/dist/scatter-chart/root/scatter-chart-root.d.ts +29 -6
  35. package/dist/scatter-chart/root/scatter-chart-root.js +18 -3
  36. package/dist/select/root/select-root.d.ts +4 -1
  37. package/dist/select/root/select-root.js +1 -0
  38. package/dist/sparkline/root/sparkline-root.d.ts +29 -4
  39. package/dist/sparkline/root/sparkline-root.js +8 -1
  40. package/dist/tag/group/tag-group.d.ts +4 -3
  41. package/dist/tag/group/tag-group.js +6 -5
  42. package/dist/tag/root/tag-root.js +2 -1
  43. package/dist/time-input/root/time-input-root.d.ts +7 -1
  44. package/dist/time-input/root/time-input-root.js +6 -3
  45. package/dist/tree/item/tree-item.d.ts +4 -4
  46. package/dist/tree/item/tree-item.js +5 -1
  47. package/lint.json +1 -1
  48. package/package.json +5 -5
  49. package/types/jsx.d.ts +688 -382
  50. package/types/vue.d.ts +101 -22
  51. package/types/words.d.ts +26 -2
@@ -1,4 +1,4 @@
1
- import { type GraphSignalDetail, HelixElement, type ViewportPanDetail, type ViewportZoomDetail } from '@fusion.dev/helix-core';
1
+ import { type GraphSignalDetail, HelixElement, KeyboardNavigationController, type ViewportPanDetail, type ViewportZoomDetail } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { type GraphLink, type GraphNode } from './graph-model.js';
4
4
  declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.dev/helix-core").ViewportElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & typeof HelixElement;
@@ -22,7 +22,9 @@ declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.
22
22
  *
23
23
  * Set `legend` to name the groups over the drawing, each a button that shows
24
24
  * or hides its notes. `hiddenGroups` holds the groups turned off, and the
25
- * list holds only what the drawing shows.
25
+ * list holds only what the drawing shows. The legend is a toolbar and one tab
26
+ * stop: the arrow keys, Home, End and a group's first letters move between
27
+ * its buttons, and Space or Enter presses one.
26
28
  *
27
29
  * `nodes` and `links` are properties, never markup: a graph of ten thousand
28
30
  * notes as ten thousand elements would cost what the renderer exists to save.
@@ -75,7 +77,7 @@ declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.
75
77
  * @csspart gesture-hint - Words over the graph for a moment when it lets a gesture pass to the page, such as how to zoom it.
76
78
  * @csspart gesture-status - The line shown while a graph with `gestures="activate"` has the gestures.
77
79
  * @csspart header - The strip above the graph.
78
- * @csspart legend - The groups over the drawing, with `legend`, each a button that shows or hides its notes.
80
+ * @csspart legend - The groups over the drawing, with `legend`: a toolbar of buttons, each showing or hiding its notes, which is one tab stop.
79
81
  * @csspart legend-item - One group in the legend, pressed while its notes are shown. Carries `data-tone` where every note in the group has the same tone, which colors the notes over the group's own color.
80
82
  * @csspart legend-item-hidden - A group in the legend whose notes are hidden.
81
83
  * @csspart legend-swatch - The mark in a group's color, before its name.
@@ -219,6 +221,12 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
219
221
  searchLabel: string;
220
222
  /** How the selected note is described in the list. */
221
223
  selectedLabel: string;
224
+ /**
225
+ * What screen readers hear after a reader selects a note, in the list or in
226
+ * the drawing, with `{note}` for its name and `{count}` for how many notes
227
+ * the list then holds, written with `count-label`.
228
+ */
229
+ selectionLabel: string;
222
230
  /** The hint shown when one finger swipes over the graph with `gestures="cooperative"`. */
223
231
  twoFingerHintLabel: string;
224
232
  /** The line shown while the graph, with `gestures="activate"`, has the gestures. */
@@ -263,6 +271,21 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
263
271
  private asked;
264
272
  /** Whether the next change of the list's rows is one a reader asked for by the legend, whose count is then said. */
265
273
  private countAsked;
274
+ /**
275
+ * The note a reader last asked to select, in the list or in the drawing,
276
+ * until it is selected: by the frame, or by a page that refused the request
277
+ * and wrote `active` itself.
278
+ */
279
+ private selectionAsked;
280
+ /** The legend's button that holds its one tab stop, by place: the one a reader was last on, or the first. */
281
+ private legendStop;
282
+ /**
283
+ * The arrow keys, Home, End and typed letters among the legend's buttons,
284
+ * so a legend of twelve groups is one tab stop, as the `Legend` family's
285
+ * toolbar is. Only a press that starts on a legend button moves: the frame's
286
+ * own arrows pan the drawing, and the list's walk the notes.
287
+ */
288
+ protected readonly legendKeys: KeyboardNavigationController;
266
289
  private readonly virtualize;
267
290
  /**
268
291
  * @internal The notes the drawing shows, which `connectGraph` reads: those
@@ -343,6 +366,8 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
343
366
  * names what every note is, and turning it off empties the graph.
344
367
  */
345
368
  private renderLegend;
369
+ /** Asks for a note to be selected, and remembers the ask, so the selection is said once it lands. */
370
+ requestActive(next: string, cause?: string): boolean;
346
371
  /** Selects the note a row names, through the request a page can refuse. */
347
372
  private choose;
348
373
  /**
@@ -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, customElement, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, MixinControllerActive, MixinViewport, scrollItemIntoView, scrollRoom, Typeahead, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { auditName, customElement, focusItemOf, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, KeyboardNavigationController, MixinControllerActive, MixinViewport, scrollItemIntoView, scrollRoom, Typeahead, typeaheadBy, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { list, magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -39,6 +39,7 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
39
39
  this.roleLabel = 'graph';
40
40
  this.searchLabel = 'Find a note';
41
41
  this.selectedLabel = 'Selected';
42
+ this.selectionLabel = '{note}, selected. {count}';
42
43
  this.twoFingerHintLabel = 'Use two fingers to move the graph';
43
44
  this.usingLabel = 'Using the graph. Press Escape to scroll the page again.';
44
45
  this.wheelHintLabel = 'Use {key} and scroll to zoom the graph';
@@ -54,6 +55,15 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
54
55
  this.whole = false;
55
56
  this.said = '';
56
57
  this.countAsked = false;
58
+ this.legendStop = 0;
59
+ this.legendKeys = new KeyboardNavigationController(this, {
60
+ items: () => [
61
+ ...(this.renderRoot?.querySelectorAll?.('[part~="legend-item"]') ?? []),
62
+ ].map((button) => focusItemOf(button, { words: button.textContent?.trim() ?? '' })),
63
+ origin: 'focus',
64
+ selects: () => false,
65
+ typeahead: typeaheadBy((item) => item.words ?? ''),
66
+ });
57
67
  this.virtualize = new VirtualizeController(this, {
58
68
  container: () => this.partNamed('list'),
59
69
  count: () => this.entries.length,
@@ -208,6 +218,13 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
208
218
  this.countAsked = false;
209
219
  this.say(this.countText());
210
220
  }
221
+ if (changed.has('active') && this.active !== '' && this.active === this.selectionAsked) {
222
+ this.selectionAsked = undefined;
223
+ const chosen = this.nodes.find((node) => node.id === this.active);
224
+ if (chosen) {
225
+ this.say(formatWords(this, 'selectionLabel', { count: this.countText(), note: nameOf(chosen) }));
226
+ }
227
+ }
211
228
  }
212
229
  }
213
230
  updated(changed) {
@@ -267,21 +284,32 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
267
284
  return nothing;
268
285
  }
269
286
  const hidden = new Set(this.hiddenGroups);
270
- return html `<div part="legend" role="group" aria-label=${this.legendLabel}>
271
- ${this.groups.map(({ name, tone }) => {
287
+ const stop = Math.min(this.legendStop, this.groups.length - 1);
288
+ return html `<div part="legend" role="toolbar" aria-label=${this.legendLabel}>
289
+ ${this.groups.map(({ name, tone }, index) => {
272
290
  const shown = !hidden.has(name);
273
291
  return html `<button
274
292
  part="legend-item${shown ? '' : ' legend-item-hidden'}"
275
293
  type="button"
294
+ tabindex=${index === stop ? '0' : '-1'}
276
295
  aria-pressed=${shown ? 'true' : 'false'}
277
296
  data-tone=${tone ?? nothing}
278
297
  @click=${() => this.toggleGroup(name)}
298
+ @focus=${() => {
299
+ this.legendStop = index;
300
+ }}
279
301
  >
280
302
  <span part="legend-swatch" aria-hidden="true"></span>${name}
281
303
  </button>`;
282
304
  })}
283
305
  </div>`;
284
306
  }
307
+ requestActive(next, cause) {
308
+ if (next !== this.active) {
309
+ this.selectionAsked = next;
310
+ }
311
+ return super.requestActive(next, cause);
312
+ }
285
313
  choose(index) {
286
314
  const entry = this.entries[index];
287
315
  if (entry) {
@@ -583,6 +611,10 @@ __decorate([
583
611
  words,
584
612
  property({ attribute: 'selected-label' })
585
613
  ], HelixHeadlessGraph.prototype, "selectedLabel", void 0);
614
+ __decorate([
615
+ words,
616
+ property({ attribute: 'selection-label' })
617
+ ], HelixHeadlessGraph.prototype, "selectionLabel", void 0);
586
618
  __decorate([
587
619
  words,
588
620
  property({ attribute: 'two-finger-hint-label' })
@@ -615,6 +647,9 @@ __decorate([
615
647
  __decorate([
616
648
  state()
617
649
  ], HelixHeadlessGraph.prototype, "said", void 0);
650
+ __decorate([
651
+ state()
652
+ ], HelixHeadlessGraph.prototype, "legendStop", void 0);
618
653
  HelixHeadlessGraph = __decorate([
619
654
  customElement('helix-headless-graph')
620
655
  ], HelixHeadlessGraph);
@@ -44,11 +44,25 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
44
44
  * `multiple` any number. Space, Enter or a press selects the cell the keys or
45
45
  * the pointer rest on, or lets it go, and under `multiple` Shift with an arrow,
46
46
  * Home, End, PageUp or PageDown takes in every cell from the last one pressed
47
- * to where the key went. `value` holds the selected days, by the names their
48
- * rows have in the table, separated by commas. A change asks with
47
+ * to where the key went. `value` holds the selected days, separated by
48
+ * commas: each by its row's `<time datetime>`, such as `2026-06-11`, or by
49
+ * its row's name where the row carries no date. A change asks with
49
50
  * `helix-before-change` and tells with `change`, and the `selection` part says
50
51
  * how many are selected, in `selection-label`.
51
52
  *
53
+ * **Keyboard.** The strip is one tab stop once it has cells.
54
+ *
55
+ * | Key | What it does |
56
+ * |---|---|
57
+ * | Tab | Moves focus to the strip and shows the first cell. |
58
+ * | Down, Up | Moves to the next or the previous cell. |
59
+ * | Right, Left | Moves a column on or back, a week with `rows="7"`. On a right-to-left page, Left moves on. |
60
+ * | Home, End | Moves to the first or the last cell. |
61
+ * | Page Down, Page Up | Moves a month on or back where the rows carry dates, and four columns otherwise. |
62
+ * | Enter, Space | Selects the cell, or lets it go, under `selection-mode`. |
63
+ * | Shift with an arrow, Home, End, Page Up or Page Down | Selects every cell from the last one pressed to where the key went, under `selection-mode="multiple"`. |
64
+ * | Escape | Puts the cell's tooltip away. |
65
+ *
52
66
  * @since 0.1.0
53
67
  * @status stable
54
68
  * @category charts
@@ -76,6 +90,7 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
76
90
  * @csspart start - The leading slot's box.
77
91
  * @csspart strip - The grid of cells, which takes focus to be read by key. It is named by the host's `aria-labelledby` or `aria-label`, then by `label`, then by the caption of the table.
78
92
  * @csspart summary - The middle slot's box.
93
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
79
94
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
80
95
  * @csspart tooltip-anchor - The `Tooltip` element itself.
81
96
  * @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
@@ -107,6 +122,9 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
107
122
  * @cssproperty --heatmap-tone-primary - The color of the `primary` tone.
108
123
  * @cssproperty --heatmap-tone-success - The color of the `success` tone, named in `tones` or in `data-tone`.
109
124
  * @cssproperty --heatmap-tone-warning - The color of the `warning` tone.
125
+ *
126
+ * @cssstate active - While a cell is shown, under the pointer or the keys, with its tooltip open.
127
+ * @cssstate empty - While the `empty` part stands in place of the cells.
110
128
  */
111
129
  export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
112
130
  /**
@@ -220,8 +238,9 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
220
238
  */
221
239
  totalLabel: string;
222
240
  /**
223
- * The selected cells, by the names their rows have in the table, such as
224
- * `2026-06-11`, separated by commas.
241
+ * The selected cells, separated by commas: each by the date its row's
242
+ * header carries in a `<time datetime>`, such as `2026-06-11`, or by the
243
+ * header's words where it carries none.
225
244
  *
226
245
  * Set it to start with cells selected. Written by a press or a key under
227
246
  * `selection-mode`.
@@ -279,7 +298,11 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
279
298
  private anchor;
280
299
  /** The region a change of selection is said in, rendered from the first change. */
281
300
  private readonly status;
282
- /** The name each drawn cell stands for in `value`: its row's name in the table. */
301
+ /**
302
+ * The name each drawn cell stands for in `value`: the date its row's header
303
+ * carries in a `<time datetime>`, or the header's words where it carries
304
+ * none.
305
+ */
283
306
  private keysOf;
284
307
  /**
285
308
  * Moves `value` to `next`, asking first: `helix-before-change` carries it,
@@ -82,7 +82,10 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
82
82
  keysOf(cells) {
83
83
  const offset = this.points.length - cells.length;
84
84
  const rows = this.sourceRows;
85
- return cells.map((_, index) => rows[offset + index]?.label ?? '');
85
+ return cells.map((_, index) => {
86
+ const row = rows[offset + index];
87
+ return row?.when ?? row?.label ?? '';
88
+ });
86
89
  }
87
90
  choose(next) {
88
91
  if (next === this.value) {
@@ -181,6 +184,8 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
181
184
  super.updated(changed);
182
185
  this.markEmpty();
183
186
  this.measure();
187
+ this.toggleState('active', this.pointer.at >= 0);
188
+ this.toggleState('empty', this.drawn.length === 0 && this.sourceKnown);
184
189
  const showing = this.visible;
185
190
  const written = showing > 0 ? String(showing) : null;
186
191
  if (this.getAttribute('visible') !== written) {
@@ -242,7 +247,7 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
242
247
  <span part="end"><slot name="end"></slot></span>
243
248
  </div>${readable ? this.renderOverview(cells) : nothing}${this.status.spoken
244
249
  ? html `<span part="selection" role="status">${this.status.said}</span>`
245
- : nothing}`;
250
+ : nothing}${this.renderTable()}`;
246
251
  }
247
252
  renderOverview(cells) {
248
253
  const rows = this.sourceRows;
@@ -55,6 +55,18 @@ declare const HelixHeadlessLineChart_base: typeof HelixElement & (new (...args:
55
55
  * there until readings arrive. The `empty` and `loading` slots take markup in
56
56
  * their place, such as a link to add the first reading.
57
57
  *
58
+ * **Keyboard.** The plot is one tab stop.
59
+ *
60
+ * | Key | What it does |
61
+ * |---|---|
62
+ * | Tab | Moves focus to the plot and shows the first reading. |
63
+ * | Right, Left | Moves to the next or the previous reading. On a right-to-left page, Left moves to the next. |
64
+ * | Down, Up | Moves among the series of the reading. With one series, moves to the next or the previous reading. |
65
+ * | Home, End | Moves to the first or the last reading. |
66
+ * | Page Down, Page Up | Moves a tenth of the readings on or back. |
67
+ * | Enter, Space | Presses the reading shown, which sends `helix-reading-press`. |
68
+ * | Escape | Puts the reading and its tooltip away. |
69
+ *
58
70
  * @since 0.1.0
59
71
  * @status stable
60
72
  * @category charts
@@ -83,9 +95,10 @@ declare const HelixHeadlessLineChart_base: typeof HelixElement & (new (...args:
83
95
  * @csspart mark - The invisible point the tooltip is anchored to.
84
96
  * @csspart marker - The mark on one reading, under `markers`, and on a reading with a blank on either side. Placed with `inset-inline-start` and `inset-block-start`, and carries its series color as `--chart-series`.
85
97
  * @csspart marker-lone - A `marker` on a reading with a blank on either side, which no line reaches. Set alongside `marker`.
86
- * @csspart plot - The drawing, which takes focus to be read by key.
98
+ * @csspart plot - The drawing, which takes focus to be read by key. With nothing to draw, a group named as the chart is, holding the `empty` or `loading` part.
87
99
  * @csspart summary - The chart's name and each series' latest reading, written as a sentence for screen readers. Hide it visually.
88
100
  * @csspart svg - The drawing surface the lines and areas sit on.
101
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
89
102
  * @csspart terminus - The mark on one series' last reading.
90
103
  * @csspart tick - One number on the axis, placed with `inset-block-start` at its level.
91
104
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
@@ -108,6 +121,10 @@ declare const HelixHeadlessLineChart_base: typeof HelixElement & (new (...args:
108
121
  * @cssproperty --chart-terminus-x - Where a series' last reading sits horizontally, as a percentage. Written by the component.
109
122
  * @cssproperty --chart-terminus-y - Where a series' last reading sits vertically, as a percentage. Written by the component.
110
123
  * @cssproperty --line-chart-axis - The width of the axis, in `ch`, set from the widest number on it. Written by the component on the axis.
124
+ *
125
+ * @cssstate active - While a reading is shown, under the pointer or the keys, with its tooltip open.
126
+ * @cssstate empty - While the `empty` part stands in the plot.
127
+ * @cssstate loading - While the `loading` part stands in the plot.
111
128
  */
112
129
  export declare class HelixHeadlessLineChart extends HelixHeadlessLineChart_base {
113
130
  /**
@@ -59,13 +59,13 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartStat
59
59
  const series = this.series;
60
60
  const readings = series.length === 0 ? [] : this.sourceRows.map((row) => row.label);
61
61
  if (readings.length === 0) {
62
- return html `<div id=${CHART_PLOT} part="plot">${this.renderStatus()}</div>`;
62
+ return this.renderEmpty();
63
63
  }
64
64
  const domain = this.range(series);
65
65
  const y = linear(domain, [PLOT_HEIGHT, 0]);
66
66
  const slot = PLOT_WIDTH / readings.length;
67
67
  const x = linear([0, Math.max(readings.length - 1, 1)], [slot / 2, PLOT_WIDTH - slot / 2]);
68
- return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}${this.renderSummary(series)}`;
68
+ return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}${this.renderSummary(series)}${this.renderTable()}`;
69
69
  }
70
70
  renderAxis(y, domain) {
71
71
  return drawValueAxis(domain, (value) => (y(value) / PLOT_HEIGHT) * 100, formatNumbers(wordsLocale(this)), (widest) => `--line-chart-axis: ${widest}ch`);
@@ -34,6 +34,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
34
34
  * hidden, and silent, for that many milliseconds, and appears only if the work
35
35
  * is still going. While it waits it carries `:state(waiting)`.
36
36
  *
37
+ * **The loader can stand in for what it wraps.** Put the content in the
38
+ * `content` slot, such as a button or a card, and set `idle` while no work is
39
+ * in progress. While the work runs, the content keeps its size and is hidden
40
+ * and inert, and the ring draws over it. While `idle` is set, the loader draws
41
+ * nothing of its own, has no role, and marks no region busy, so only the
42
+ * content shows. Clearing `idle` starts `delay` again.
43
+ *
37
44
  * @since 0.1.0
38
45
  * @status stable
39
46
  * @category feedback
@@ -41,7 +48,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
41
48
  * @documentation https://fusion.dev/helix/components/loader
42
49
  *
43
50
  * @slot - An icon or logo that stays still at the center. Empty by default.
51
+ * @slot content - What the loader stands in for while the work runs, such as a button. It keeps its size, hidden and inert, until `idle` is set.
44
52
  *
53
+ * @csspart content - The box that holds the `content` slot. Hidden while the work runs, and shown while `idle` is set.
45
54
  * @csspart indicator - The arc that turns.
46
55
  * @csspart mark - The region that holds the slotted content at the center.
47
56
  * @csspart svg - The rotating circle pair.
@@ -71,6 +80,12 @@ export declare class HelixHeadlessLoader extends HelixElement {
71
80
  * way `Tooltip.for` does.
72
81
  */
73
82
  for: string;
83
+ /**
84
+ * Whether no work is in progress. The loader draws nothing of its own, has
85
+ * no role, and marks no region busy, and what is in the `content` slot
86
+ * shows. Clearing it starts `delay` again.
87
+ */
88
+ idle: boolean;
74
89
  /**
75
90
  * The loader's name, announced by screen readers while the work is in
76
91
  * progress.
@@ -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
  import { customElement, DelayController, elementById, HelixElement, hostAria, reportRuntime, words, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
8
+ import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { awaitRegion, claimBusy, releaseBusy } from '../busy.js';
11
11
  let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
@@ -13,6 +13,7 @@ let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
13
13
  super(...arguments);
14
14
  this.delay = 0;
15
15
  this.for = '';
16
+ this.idle = false;
16
17
  this.label = 'Loading';
17
18
  this.due = new DelayController(this);
18
19
  this.region = null;
@@ -20,7 +21,7 @@ let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
20
21
  }
21
22
  static { this.words = 'helix-loader'; }
22
23
  get waiting() {
23
- return this.delay > 0 && !this.due.elapsed;
24
+ return !this.idle && this.delay > 0 && !this.due.elapsed;
24
25
  }
25
26
  connectedCallback() {
26
27
  super.connectedCallback();
@@ -46,7 +47,7 @@ let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
46
47
  mark() {
47
48
  this.stopAwaiting?.();
48
49
  this.stopAwaiting = undefined;
49
- const named = this.isConnected && this.for !== '';
50
+ const named = this.isConnected && this.for !== '' && !this.idle;
50
51
  const region = named ? elementById(this, this.for) : null;
51
52
  if (named && !region) {
52
53
  this.stopAwaiting = awaitRegion(this.getRootNode(), () => elementById(this, this.for), () => this.mark());
@@ -73,16 +74,34 @@ let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
73
74
  this.found = false;
74
75
  this.mark();
75
76
  }
77
+ if (changed.has('idle') && changed.get('idle') !== undefined) {
78
+ if (!this.idle && this.delay > 0) {
79
+ this.due.start(this.delay);
80
+ }
81
+ this.mark();
82
+ }
76
83
  }
77
84
  willUpdate(changed) {
78
85
  super.willUpdate(changed);
79
- hostAria(this.internals, { label: this.label, role: 'progressbar' });
80
- this.internals.ariaBusy = 'true';
86
+ hostAria(this.internals, {
87
+ label: this.idle ? '' : this.label,
88
+ role: this.idle ? '' : 'progressbar',
89
+ });
90
+ this.internals.ariaBusy = this.idle ? null : 'true';
81
91
  this.internals.ariaHidden = this.waiting ? 'true' : null;
82
92
  this.toggleState('waiting', this.waiting);
83
93
  }
84
94
  render() {
85
- return html `<svg part="svg" viewBox="0 0 24 24" aria-hidden="true" ?hidden=${this.waiting}>
95
+ const content = html `<span
96
+ part="content"
97
+ ?inert=${!this.idle}
98
+ style=${this.idle ? nothing : 'visibility: hidden'}
99
+ ><slot name="content"></slot
100
+ ></span>`;
101
+ if (this.idle) {
102
+ return content;
103
+ }
104
+ return html `${content}<svg part="svg" viewBox="0 0 24 24" aria-hidden="true" ?hidden=${this.waiting}>
86
105
  <circle part="track" cx="12" cy="12" r="10" fill="none" />
87
106
  <circle part="indicator" cx="12" cy="12" r="10" fill="none" />
88
107
  </svg>
@@ -95,6 +114,9 @@ __decorate([
95
114
  __decorate([
96
115
  property()
97
116
  ], HelixHeadlessLoader.prototype, "for", void 0);
117
+ __decorate([
118
+ property({ type: Boolean })
119
+ ], HelixHeadlessLoader.prototype, "idle", void 0);
98
120
  __decorate([
99
121
  words,
100
122
  property()
@@ -133,6 +133,7 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
133
133
  }
134
134
  this.placeCaret(true);
135
135
  if (applied && this.value === code && code.length === this.count) {
136
+ this.runPendingUpdate();
136
137
  this.dispatchEvent(new HelixCompleteEvent({ value: code }));
137
138
  this.commit();
138
139
  if (this.autosubmit) {
@@ -30,7 +30,8 @@ declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion
30
30
  *
31
31
  * Left and Right swap on a right-to-left page. The marks are a `radiogroup`
32
32
  * named by `label`, and each score is a `radio` named by `option-label`, such
33
- * as "3 out of 5". A read-only rating is one `img` named by its score.
33
+ * as "3 out of 5". A read-only rating is one `img` named by its label and
34
+ * then its score, such as "Quality 3 out of 5".
34
35
  *
35
36
  * @since 0.1.0
36
37
  * @status stable
@@ -21,6 +21,11 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
21
21
  this.valueMissingMessage = 'Choose a rating to continue.';
22
22
  this.valueText = '';
23
23
  this.aria = new AriaForwardController(this, {
24
+ labelledByAfter: () => {
25
+ const score = this.readOnly ? this.renderRoot?.querySelector('#score') : null;
26
+ return score ? [score] : [];
27
+ },
28
+ named: () => this.labeled,
24
29
  target: () => this.partNamed('marks'),
25
30
  });
26
31
  this.mark = new SlottedGlyph();
@@ -255,8 +260,8 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
255
260
  <div
256
261
  part="marks"
257
262
  role=${this.readOnly ? 'img' : 'radiogroup'}
258
- aria-label=${this.readOnly ? said : nothing}
259
- aria-labelledby=${!this.readOnly && this.labeled ? this.labelId : nothing}
263
+ aria-label=${this.readOnly && !this.labeled ? said : nothing}
264
+ aria-labelledby=${this.labeled ? (this.readOnly ? `${this.labelId} score` : this.labelId) : nothing}
260
265
  aria-describedby=${this.describedBy ?? nothing}
261
266
  aria-invalid=${this.invalidity ?? nothing}
262
267
  aria-required=${this.required && !this.readOnly ? 'true' : nothing}
@@ -317,6 +322,7 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
317
322
  >${showing}</span
318
323
  >`
319
324
  : nothing}
325
+ ${this.readOnly ? html `<span id="score" hidden>${said}</span>` : nothing}
320
326
  </div>
321
327
  ${this.renderHint()}${this.renderFeedback()}`;
322
328
  }
@@ -51,10 +51,10 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
51
51
  * readers.
52
52
  *
53
53
  * **The plot is read by key.** It takes focus, named by `label` or the caption.
54
- * The left and right arrows move from row to row in the table's order, Home
55
- * and End go to the first and the last, and the up and down arrows move among
56
- * the series of a row. The tooltip sits on that point, the `live` part says the
57
- * row in words, and Escape puts the tooltip away.
54
+ * The left and right arrows move from point to point, left to right across
55
+ * the plot, Home and End go to the first and the last, and the up and down
56
+ * arrows move among the series of a row. The tooltip sits on that point, the
57
+ * `live` part says the row in words, and Escape puts the tooltip away.
58
58
  *
59
59
  * Each point is a `<span>` placed by percentage over the drawing, as
60
60
  * `LineChart` places its end points, so points stay round when the drawing
@@ -81,6 +81,18 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
81
81
  * there until readings arrive. The `empty` and `loading` slots take markup in
82
82
  * their place, such as a link to add the first reading.
83
83
  *
84
+ * **Keyboard.** The plot is one tab stop.
85
+ *
86
+ * | Key | What it does |
87
+ * |---|---|
88
+ * | Tab | Moves focus to the plot and shows the first reading. |
89
+ * | Right, Left | Moves to the next or the previous point, left to right across the plot. On a right-to-left page, Left moves to the next. |
90
+ * | Down, Up | Moves among the series of the point. With one series, moves to the next or the previous point. |
91
+ * | Home, End | Moves to the first or the last point. |
92
+ * | Page Down, Page Up | Moves a tenth of the points on or back. |
93
+ * | Enter, Space | Presses the point shown, which sends `helix-reading-press`. |
94
+ * | Escape | Puts the point and its tooltip away. |
95
+ *
84
96
  * @since 0.1.0
85
97
  * @status stable
86
98
  * @category charts
@@ -99,11 +111,12 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
99
111
  * @csspart loading - The words in place of the drawing while `loading` is set and there are no readings: the `loading` slot, falling back to `loading-text`.
100
112
  * @csspart mark - The invisible point the tooltip is anchored to.
101
113
  * @csspart plane - The grid that holds the plot, both axes, and their names. It runs left to right on every page.
102
- * @csspart plot - The drawing, which takes focus to be read by key.
114
+ * @csspart plot - The drawing, which takes focus to be read by key. With nothing to draw, a group named as the chart is, holding the `empty` or `loading` part.
103
115
  * @csspart point - One row, as a point.
104
116
  * @csspart point-active - A point in the row under the pointer. Set alongside `point`.
105
117
  * @csspart summary - The text that says how strongly the measures relate, for people who cannot see the points.
106
118
  * @csspart svg - The drawing surface the gridlines and the trend lines sit on.
119
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
107
120
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
108
121
  * @csspart tooltip-anchor - The `Tooltip` element itself.
109
122
  * @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
@@ -123,7 +136,7 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
123
136
  * @csspart trend - One series' line of best fit, under `trend`. Carries its series color as `--chart-series`.
124
137
  * @csspart x-axis - The values along the horizontal axis, under the plot.
125
138
  * @csspart x-name - The name of the horizontal measure.
126
- * @csspart x-tick - One labeled value on the horizontal axis. Each axis has its own tick part, because `::part()` cannot tell which axis a tick is on.
139
+ * @csspart x-tick - One labeled value on the horizontal axis. Each axis has its own tick part, because `::part()` cannot tell which axis a tick is on. The one at the far edge carries `data-edge="end"`.
127
140
  * @csspart y-axis - The values along the vertical axis, beside the plot.
128
141
  * @csspart y-name - The name of the vertical measure.
129
142
  * @csspart y-tick - One labeled value on the vertical axis.
@@ -132,6 +145,10 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
132
145
  * @cssproperty --chart-point-x - A point's horizontal position, as a percentage. The component writes it.
133
146
  * @cssproperty --chart-point-y - A point's vertical position, as a percentage from the top. The component writes it.
134
147
  * @cssproperty --chart-tick - A tick's position along its axis, as a percentage. The component writes it.
148
+ *
149
+ * @cssstate active - While a reading is shown, under the pointer or the keys, with its tooltip open.
150
+ * @cssstate empty - While the `empty` part stands in the plot.
151
+ * @cssstate loading - While the `loading` part stands in the plot.
135
152
  */
136
153
  export declare class HelixHeadlessScatterChart extends HelixHeadlessScatterChart_base {
137
154
  /**
@@ -228,6 +245,12 @@ export declare class HelixHeadlessScatterChart extends HelixHeadlessScatterChart
228
245
  * a point with one coordinate has nowhere to be.
229
246
  */
230
247
  protected get field(): ScatterField;
248
+ /**
249
+ * @internal The rows in the order the keys walk them: by the measure across,
250
+ * left to right, so a reader moves through the points as they are drawn. A
251
+ * row with nothing across comes last, in the table's order.
252
+ */
253
+ readingOrder(): number[];
231
254
  protected render(): TemplateResult;
232
255
  /** The rules and the points, behind a seam the styled tier can move. */
233
256
  protected renderPlot(field: ScatterField, along: (value: number) => number, up: (value: number) => number): TemplateResult;
@@ -135,12 +135,19 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
135
135
  yNames: ups.map((column) => name(column)),
136
136
  };
137
137
  }
138
+ readingOrder() {
139
+ const across = this.plottedColumns[0];
140
+ const rows = this.sourceRows;
141
+ const x = (at) => (across === undefined ? undefined : rows[at]?.cells[across]?.value) ??
142
+ Number.POSITIVE_INFINITY;
143
+ return rows.map((_, at) => at).sort((one, two) => x(one) - x(two) || one - two);
144
+ }
138
145
  render() {
139
146
  const field = this.field;
140
147
  this.drawn = field.points;
141
148
  if (field.points.length === 0) {
142
149
  return html `<div dir="ltr" part="plane">
143
- <div id=${CHART_PLOT} part="plot">${this.renderStatus()}</div>
150
+ ${this.renderEmpty()}
144
151
  </div>`;
145
152
  }
146
153
  const format = formatNumbers(wordsLocale(this));
@@ -149,7 +156,15 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
149
156
  return html `<div dir="ltr" part="plane">
150
157
  ${this.renderPlot(field, along, up)}
151
158
  <div aria-hidden="true" part="x-axis">
152
- ${ticks(field.x).map((level) => html `<span part="x-tick" style="--chart-tick: ${along(level)}%">${format(level)}</span>`)}
159
+ ${ticks(field.x).map((level) => {
160
+ const place = along(level);
161
+ return html `<span
162
+ data-edge=${place >= PLOT_WIDTH ? 'end' : nothing}
163
+ part="x-tick"
164
+ style="--chart-tick: ${place}%"
165
+ >${format(level)}</span
166
+ >`;
167
+ })}
153
168
  </div>
154
169
  <div aria-hidden="true" part="y-axis">
155
170
  ${ticks(field.y).map((level) => html `<span part="y-tick" style="--chart-tick: ${up(level)}%">${format(level)}</span>`)}
@@ -157,7 +172,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
157
172
  <span aria-hidden="true" dir="auto" part="x-name">${field.xName}</span>
158
173
  <span aria-hidden="true" dir="auto" part="y-name">${field.yNames.join(', ')}</span>
159
174
  </div>
160
- ${this.renderSummary(field)}`;
175
+ ${this.renderSummary(field)}${this.renderTable()}`;
161
176
  }
162
177
  renderPlot(field, along, up) {
163
178
  const at = this.pointer.at;