@fusion.dev/helix-headless 0.176.0 → 0.177.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 (39) hide show
  1. package/custom-elements.json +610 -696
  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/canvas/node/canvas-node.js +1 -1
  9. package/dist/canvas/root/canvas-root.d.ts +5 -1
  10. package/dist/canvas/root/canvas-root.js +9 -1
  11. package/dist/donut-chart/root/donut-chart-root.d.ts +37 -2
  12. package/dist/donut-chart/root/donut-chart-root.js +22 -8
  13. package/dist/element-events.generated.js +2 -0
  14. package/dist/graph/root/graph-root.d.ts +28 -3
  15. package/dist/graph/root/graph-root.js +38 -3
  16. package/dist/heatmap/root/heatmap-root.d.ts +28 -5
  17. package/dist/heatmap/root/heatmap-root.js +7 -2
  18. package/dist/line-chart/root/line-chart-root.d.ts +18 -1
  19. package/dist/line-chart/root/line-chart-root.js +2 -2
  20. package/dist/loader/root/loader-root.d.ts +15 -0
  21. package/dist/loader/root/loader-root.js +28 -6
  22. package/dist/rating/root/rating-root.d.ts +2 -1
  23. package/dist/rating/root/rating-root.js +8 -2
  24. package/dist/scatter-chart/root/scatter-chart-root.d.ts +29 -6
  25. package/dist/scatter-chart/root/scatter-chart-root.js +18 -3
  26. package/dist/select/root/select-root.d.ts +4 -1
  27. package/dist/select/root/select-root.js +1 -0
  28. package/dist/sparkline/root/sparkline-root.d.ts +29 -4
  29. package/dist/sparkline/root/sparkline-root.js +8 -1
  30. package/dist/tag/group/tag-group.d.ts +4 -3
  31. package/dist/tag/group/tag-group.js +6 -5
  32. package/dist/tag/root/tag-root.js +2 -1
  33. package/dist/tree/item/tree-item.d.ts +4 -4
  34. package/dist/tree/item/tree-item.js +5 -1
  35. package/lint.json +1 -1
  36. package/package.json +5 -5
  37. package/types/jsx.d.ts +532 -364
  38. package/types/vue.d.ts +86 -19
  39. package/types/words.d.ts +22 -0
@@ -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()
@@ -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;
@@ -2,6 +2,7 @@ import { AriaForwardController, type DataOptionItem, HelixFormElement, PopoverCo
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { type HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  import '../../listbox/index.js';
5
+ import '../../option/index.js';
5
6
  declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
6
7
  /**
7
8
  * @summary A control that opens a list of options to choose from.
@@ -13,7 +14,9 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
13
14
  * place for focus to return to when the list closes.
14
15
  *
15
16
  * Your options stay in your light DOM and are slotted through into the listbox,
16
- * so the listbox looks through slots to find them. For a list too long to draw
17
+ * so the listbox looks through slots to find them. Write them as `Option`,
18
+ * group them in `Option.Group`, and divide them with `Option.Separator`.
19
+ * Importing Select registers all three in the same tier. For a list too long to draw
17
20
  * whole, set `items` instead: the list draws only the options in sight, as
18
21
  * Listbox's `items` does, and the trigger, its keys and the form read the
19
22
  * whole set.
@@ -12,6 +12,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import { optionsAmong } from '../../option/root/option-root.js';
14
14
  import '../../listbox/index.js';
15
+ import '../../option/index.js';
15
16
  import { over } from '../align.js';
16
17
  let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
17
18
  constructor() {
@@ -24,6 +24,10 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
24
24
  * the lowest, in `overview-label`: "From 12 to 20, highest 30, lowest 8". An
25
25
  * `aria-label` on the element names it as `label` does.
26
26
  *
27
+ * `helix-reading-change` says each move of the reading, by pointer, tap or
28
+ * key, and `helix-reading-press` says a reading was clicked, tapped, or pressed
29
+ * with Enter or Space, as on every chart.
30
+ *
27
31
  * A pointer resting on the drawing shows the reading under it: the `cursor`
28
32
  * part marks it and the `readout` part writes it. `marks` adds a mark on the
29
33
  * highest and the lowest reading, and `range` draws a band between two values,
@@ -38,12 +42,26 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
38
42
  * the component computes and CSS cannot, so a stylesheet can draw the sparkline
39
43
  * without measuring anything.
40
44
  *
45
+ * **Keyboard.** A sparkline with a `label` is one tab stop.
46
+ *
47
+ * | Key | What it does |
48
+ * |---|---|
49
+ * | Tab | Moves focus to the drawing and shows the first reading, where there is a `label`. |
50
+ * | Any arrow | Right and Down move to the next reading, Left and Up to the previous. On a right-to-left page, Left moves to the next. |
51
+ * | Home, End | Moves to the first or the last reading. |
52
+ * | Page Down, Page Up | Moves a tenth of the readings on or back. |
53
+ * | Enter, Space | Presses the reading shown, which sends `helix-reading-press`. |
54
+ * | Escape | Puts the reading and its tooltip away. |
55
+ *
41
56
  * @since 0.1.0
42
57
  * @status stable
43
58
  * @category charts
44
59
  * @aliases Micro Chart, Trend Line, Inline Chart
45
60
  * @documentation https://fusion.dev/helix/components/sparkline
46
61
  *
62
+ * @event {HelixReadingChangeEvent} helix-reading-change - The reading shown moved, or was put away: `index` is the reading, or `-1` for none, with its row's `label` from a table and the `pointerType` that moved it.
63
+ * @event {HelixReadingPressEvent} helix-reading-press - A reading was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
64
+ *
47
65
  * @csspart area - The region between the line and the bottom of its range.
48
66
  * @csspart bar - One column, drawn when `plot="bar"`.
49
67
  * @csspart cursor - The mark on the reading a pointer or a key rests on, rendered only while there is one. Placed with `inset-inline-start` and `inset-block-start`.
@@ -201,17 +219,24 @@ export declare class HelixHeadlessSparkline extends HelixElement {
201
219
  values: string | readonly number[];
202
220
  protected readonly source: SourceController;
203
221
  /**
204
- * Finds the reading a pointer or a key rests on, as it does for every
205
- * chart, so a sparkline is read the way they are.
222
+ * @internal Finds the reading a pointer or a key rests on, as it does for
223
+ * every chart, so a sparkline is read the way they are.
206
224
  */
207
225
  readonly pointer: ChartPointerController;
208
226
  /**
209
- * Hears an `aria-label` written on the sparkline, which no property mirrors,
227
+ * @internal Hears an `aria-label` written on the sparkline, which no property mirrors,
210
228
  * because it names the drawing as `label` does, as on every chart.
211
229
  */
212
230
  static get observedAttributes(): string[];
213
- /** Called by the browser when an observed attribute changes; a new `aria-label` renames the drawing. */
231
+ /** @internal Called by the browser when an observed attribute changes; a new `aria-label` renames the drawing. */
214
232
  attributeChangedCallback(name: string, old: string | null, value: string | null): void;
233
+ /**
234
+ * Sends the reading's moves and presses as `helix-reading-change` and
235
+ * `helix-reading-press`, named by the table's row where there is one. Here
236
+ * and not in a constructor, where the manifest's analyzer reads the
237
+ * assignment as a member of this class.
238
+ */
239
+ connectedCallback(): void;
215
240
  /** The drawing's name: an `aria-label` written on the sparkline, then `label`. */
216
241
  get named(): string;
217
242
  /** The two values of `range`, lower first, or none. */
@@ -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 { CHART_PLOT, ChartPointerController, customElement, extent, extremesOf, formatNumbers, formatWords, HelixElement, linear, linePath, PLOT_WIDTH, SourceController, unbroken, words, wordsLocale, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
7
+ import { CHART_PLOT, ChartPointerController, customElement, extent, extremesOf, formatNumbers, formatWords, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, linear, linePath, PLOT_WIDTH, SourceController, unbroken, words, wordsLocale, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { areaPath, overviewWords, readingWords, slotsOf } from '../readings.js';
@@ -33,6 +33,13 @@ let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
33
33
  this.requestUpdate();
34
34
  }
35
35
  }
36
+ connectedCallback() {
37
+ super.connectedCallback();
38
+ this.pointer.onReading = (heard, detail) => {
39
+ const told = { ...detail, label: this.source.rows[detail.index]?.label ?? '' };
40
+ this.dispatchEvent(heard === 'press' ? new HelixReadingPressEvent(told) : new HelixReadingChangeEvent(told));
41
+ };
42
+ }
36
43
  get named() {
37
44
  return this.getAttribute?.('aria-label') || this.label;
38
45
  }
@@ -99,8 +99,9 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
99
99
  */
100
100
  selectionMode?: TagSelectionMode;
101
101
  /**
102
- * The selected tags' values, separated by commas, since a value may hold a
103
- * space.
102
+ * The selected tags' values, separated by commas with no space, as Toggle
103
+ * Group and Table write theirs, since a value may hold a space. A value
104
+ * written with a space after each comma is read the same way.
104
105
  *
105
106
  * Set it to start with tags selected. A tag's value is its `value`, or its
106
107
  * words where it has none.
@@ -159,7 +160,7 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
159
160
  /** Whether a press or a move selects the tag alone. */
160
161
  private get replaces();
161
162
  /**
162
- * Asks for `value` to become `next`, where it changes and where it leaves a
163
+ * Asks for `value` to become `asked`, where it changes and where it leaves a
163
164
  * tag selected under `disallow-empty-selection`.
164
165
  */
165
166
  private ask;
@@ -108,7 +108,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
108
108
  const left = this.tags.map((one) => this.valueFor(one));
109
109
  const pruned = prunedValue(this.written, this.valueFor(tag), left);
110
110
  if (pruned !== this.written) {
111
- this.value = pruned;
111
+ this.value = commaList(pruned).join(',');
112
112
  this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
113
113
  }
114
114
  const active = document.activeElement;
@@ -150,7 +150,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
150
150
  : this.value
151
151
  .map((one) => String(one).trim())
152
152
  .filter((one) => one !== '')
153
- .join(', ');
153
+ .join(',');
154
154
  }
155
155
  get tags() {
156
156
  return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessTag);
@@ -212,8 +212,9 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
212
212
  get replaces() {
213
213
  return this.selectionMode !== undefined && this.selectionBehavior === 'replace';
214
214
  }
215
- ask(next) {
216
- if (next === this.written) {
215
+ ask(asked) {
216
+ const next = commaList(asked).join(',');
217
+ if (next === commaList(this.written).join(',')) {
217
218
  return;
218
219
  }
219
220
  if (this.disallowEmptySelection && commaList(next).length === 0) {
@@ -239,7 +240,7 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
239
240
  this.choose(this.tags.filter((tag) => chosen.includes(tag)));
240
241
  }
241
242
  choose(tags) {
242
- this.ask([...new Set(tags.map((tag) => this.valueFor(tag)))].join(', '));
243
+ this.ask([...new Set(tags.map((tag) => this.valueFor(tag)))].join(','));
243
244
  }
244
245
  connectedCallback() {
245
246
  super.connectedCallback();
@@ -70,12 +70,13 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
70
70
  }
71
71
  render() {
72
72
  const content = html `<slot name="start" part="start"></slot>${this.renderLabel()}${this.dismissible ? this.renderDismiss() : nothing}`;
73
+ const stop = this.focusable ? '0' : this.disabled ? nothing : '-1';
73
74
  return this.grouped
74
75
  ? html `<span
75
76
  aria-label=${this.words || nothing}
76
77
  part="cell"
77
78
  role="gridcell"
78
- tabindex=${this.focusable ? '0' : '-1'}
79
+ tabindex=${stop}
79
80
  @focus=${this.handleCellFocus}
80
81
  @blur=${this.handleCellFocus}
81
82
  >${content}</span
@@ -14,9 +14,9 @@ declare const HelixHeadlessTreeItem_base: (new (...args: any[]) => import("@fusi
14
14
  * Each item is an element because it holds state: whether it is open, selected,
15
15
  * and focused.
16
16
  *
17
- * An item is a branch when something is slotted into `children`. This is
18
- * detected on the client, so server-rendered markup shows a leaf and the twisty
19
- * appears on hydration.
17
+ * An item is a branch when an item is slotted into `children`, with
18
+ * `slot="children"` on each nested item. This is detected on the client, so
19
+ * server-rendered markup shows a leaf and the twisty appears on hydration.
20
20
  *
21
21
  * **`href` makes the whole row a link**, rendered as an `<a>`. Enter, middle
22
22
  * click, and the status bar then work anywhere on the row.
@@ -43,7 +43,7 @@ declare const HelixHeadlessTreeItem_base: (new (...args: any[]) => import("@fusi
43
43
  * @aliases TreeNode, Branch, Leaf
44
44
  * @documentation https://fusion.dev/helix/components/tree
45
45
  *
46
- * @slot - The item's label.
46
+ * @slot - The item's label. A nested item does not go here: give it `slot="children"`. In a development build, an item nested here is reported.
47
47
  * @slot chevron - Replaces the expand and collapse mark of a branch, such as with your own icon.
48
48
  * @slot children - Nested items, which make this a branch.
49
49
  * @slot end - Content after the label, at the far side of the row, such as a count or a state.
@@ -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, HelixElement, HelixLoadEvent, HelixSelectEvent, MixinControllerOpen, } from '@fusion.dev/helix-core';
7
+ import { auditNesting, customElement, HelixElement, HelixLoadEvent, HelixSelectEvent, MixinControllerOpen, } from '@fusion.dev/helix-core';
8
8
  import { caretDown, check, dotsSixVertical, minus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -68,6 +68,10 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
68
68
  slot?.addEventListener('slotchange', () => {
69
69
  this.branch = slot.assignedElements().length > 0;
70
70
  });
71
+ auditNesting(this, 'children');
72
+ this.renderRoot
73
+ .querySelector('[part="label"] > slot')
74
+ ?.addEventListener('slotchange', () => auditNesting(this, 'children'));
71
75
  }
72
76
  get choosable() {
73
77
  return this.value !== '' && !this.heads && !this.checkable && !this.disabled;