@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
@@ -83,6 +83,18 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
83
83
  * there until readings arrive. The `empty` and `loading` slots take markup in
84
84
  * their place, such as a link to add the first reading.
85
85
  *
86
+ * **Keyboard.** The plot is one tab stop.
87
+ *
88
+ * | Key | What it does |
89
+ * |---|---|
90
+ * | Tab | Moves focus to the plot and shows the first reading. |
91
+ * | Right, Left | Moves to the next or the previous reading. On a right-to-left page, Left moves to the next. |
92
+ * | Down, Up | Moves among the series of the reading. With one series, moves to the next or the previous reading. |
93
+ * | Home, End | Moves to the first or the last reading. |
94
+ * | Page Down, Page Up | Moves a tenth of the readings on or back. |
95
+ * | Enter, Space | Presses the reading shown, which sends `helix-reading-press`. |
96
+ * | Escape | Puts the reading and its tooltip away. |
97
+ *
86
98
  * @since 0.1.0
87
99
  * @status stable
88
100
  * @category charts
@@ -107,9 +119,10 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
107
119
  * @csspart live - The readout: a polite live region that says the reading the keys moved to. Hide it visually.
108
120
  * @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`.
109
121
  * @csspart mark - The invisible point the tooltip is anchored to.
110
- * @csspart plot - The drawing, which takes focus to be read by key.
122
+ * @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.
111
123
  * @csspart summary - The total and each part's share at the latest reading, written as a sentence for screen readers.
112
124
  * @csspart svg - The drawing surface the bands sit on.
125
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
113
126
  * @csspart tick - One number on the axis, placed with `inset-block-start` at its level.
114
127
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
115
128
  * @csspart tooltip-anchor - The `Tooltip` element itself.
@@ -129,6 +142,10 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
129
142
  * @csspart tooltip-value - A column's value inside the tooltip.
130
143
  *
131
144
  * @cssproperty --area-chart-axis - The width of the axis, in `ch`, set from the widest number on it. Written by the component on the axis.
145
+ *
146
+ * @cssstate active - While a reading is shown, under the pointer or the keys, with its tooltip open.
147
+ * @cssstate empty - While the `empty` part stands in the plot.
148
+ * @cssstate loading - While the `loading` part stands in the plot.
132
149
  */
133
150
  export declare class HelixHeadlessAreaChart extends HelixHeadlessAreaChart_base {
134
151
  /**
@@ -96,7 +96,7 @@ let HelixHeadlessAreaChart = class HelixHeadlessAreaChart extends MixinChartStat
96
96
  const rows = this.sourceRows;
97
97
  const stack = this.stack;
98
98
  if (rows.length === 0 || stack.columns.length === 0) {
99
- return html `<div id=${CHART_PLOT} part="plot">${this.renderStatus()}</div>`;
99
+ return this.renderEmpty();
100
100
  }
101
101
  const top = this.overlap
102
102
  ? Math.max(0, ...stack.layers.flat())
@@ -108,7 +108,7 @@ let HelixHeadlessAreaChart = class HelixHeadlessAreaChart extends MixinChartStat
108
108
  const slot = PLOT_WIDTH / rows.length;
109
109
  const x = linear([0, Math.max(rows.length - 1, 1)], [slot / 2, PLOT_WIDTH - slot / 2]);
110
110
  return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(stack, rows.length, x, y, domain)}
111
- ${this.renderLabels(rows.map((row) => row.label))} ${this.renderSummary(stack, rows)}`;
111
+ ${this.renderLabels(rows.map((row) => row.label))} ${this.renderSummary(stack, rows)}${this.renderTable()}`;
112
112
  }
113
113
  renderAxis(y, domain) {
114
114
  const locale = wordsLocale(this);
@@ -32,7 +32,7 @@ export declare const initialsOf: (name: string) => string;
32
32
  * @aliases Profile picture, Gravatar
33
33
  * @documentation https://fusion.dev/helix/components/avatar
34
34
  *
35
- * @slot - Shown when there is no usable image, in place of the initials.
35
+ * @slot - Shown when there is no usable image, in place of the initials. Hidden from assistive technology when the avatar has a `name`, which the host announces.
36
36
  *
37
37
  * @event {Event} error - Relayed from the image when it fails.
38
38
  * @event {Event} load - Relayed from the image once it has loaded.
@@ -103,7 +103,7 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
103
103
  return this.renderInitials();
104
104
  }
105
105
  renderInitials() {
106
- return html `<slot>
106
+ return html `<slot aria-hidden=${this.name === '' ? nothing : 'true'}>
107
107
  <span part="initials" aria-hidden="true">${this.initials ?? initialsOf(this.name)}</span>
108
108
  </slot>`;
109
109
  }
@@ -45,6 +45,19 @@ declare const HelixHeadlessBarChart_base: typeof HelixElement & (new (...args: a
45
45
  * there until readings arrive. The `empty` and `loading` slots take markup in
46
46
  * their place, such as a link to add the first reading.
47
47
  *
48
+ * **Keyboard.** The plot is one tab stop.
49
+ *
50
+ * | Key | What it does |
51
+ * |---|---|
52
+ * | Tab | Moves focus to the plot and shows the first reading. |
53
+ * | Right, Left | Moves to the next or the previous reading. On a right-to-left page, Left moves to the next. |
54
+ * | Down, Up | Moves among the series of the reading. With one series, moves to the next or the previous reading. |
55
+ * | Down, Up under `orientation="horizontal"` | Moves to the next or the previous reading, and Right and Left move among the series. |
56
+ * | Home, End | Moves to the first or the last reading. |
57
+ * | Page Down, Page Up | Moves a tenth of the readings on or back. |
58
+ * | Enter, Space | Presses the reading shown, which sends `helix-reading-press`. |
59
+ * | Escape | Puts the reading and its tooltip away. |
60
+ *
48
61
  * @since 0.1.0
49
62
  * @status stable
50
63
  * @category charts
@@ -73,9 +86,10 @@ declare const HelixHeadlessBarChart_base: typeof HelixElement & (new (...args: a
73
86
  * @csspart live - The readout: a polite live region that says the reading the keys moved to. Hide it visually.
74
87
  * @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`.
75
88
  * @csspart mark - The invisible point the tooltip is anchored to.
76
- * @csspart plot - The drawing, which takes focus to be read by key.
89
+ * @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.
77
90
  * @csspart summary - The chart's name and each series' highest reading, written as a sentence for screen readers. Hide it visually.
78
91
  * @csspart svg - The drawing surface the bars and lines sit on.
92
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
79
93
  * @csspart tick - One number on an axis.
80
94
  * @csspart tooltip - The tooltip bubble, forwarded from `Tooltip`.
81
95
  * @csspart tooltip-anchor - The `Tooltip` element itself.
@@ -99,6 +113,10 @@ declare const HelixHeadlessBarChart_base: typeof HelixElement & (new (...args: a
99
113
  * @cssproperty --chart-bar-wash - The gradient for a bar, named per bar so the styled
100
114
  * tier can use it as the `fill`. It is always drawn, and used only when
101
115
  * `wash="gradient"` is set.
116
+ *
117
+ * @cssstate active - While a reading is shown, under the pointer or the keys, with its tooltip open.
118
+ * @cssstate empty - While the `empty` part stands in the plot.
119
+ * @cssstate loading - While the `loading` part stands in the plot.
102
120
  */
103
121
  export declare class HelixHeadlessBarChart extends HelixHeadlessBarChart_base {
104
122
  /**
@@ -44,7 +44,7 @@ let HelixHeadlessBarChart = class HelixHeadlessBarChart extends MixinChartStatus
44
44
  const series = this.series;
45
45
  const categories = series.length === 0 ? [] : this.sourceRows.map((row) => row.label);
46
46
  if (categories.length === 0) {
47
- return html `<div id=${CHART_PLOT} part="plot">${this.renderStatus()}</div>`;
47
+ return this.renderEmpty();
48
48
  }
49
49
  const scaled = this.scaled;
50
50
  const own = (one) => scaled.has(seriesKey(one.name));
@@ -78,7 +78,7 @@ let HelixHeadlessBarChart = class HelixHeadlessBarChart extends MixinChartStatus
78
78
  : undefined;
79
79
  return html `${z === undefined && !this.valueAxis
80
80
  ? nothing
81
- : this.renderAxis(y, domain, 'primary', sideways, widest)}${this.renderPlot(series, categories, y, domain, floor, z)}${z === undefined ? nothing : this.renderAxis(z, other, 'secondary', sideways, widest, tint)}${this.renderLabels(categories)}${this.renderSummary(series)}`;
81
+ : this.renderAxis(y, domain, 'primary', sideways, widest)}${this.renderPlot(series, categories, y, domain, floor, z)}${z === undefined ? nothing : this.renderAxis(z, other, 'secondary', sideways, widest, tint)}${this.renderLabels(categories)}${this.renderSummary(series)}${this.renderTable()}`;
82
82
  }
83
83
  renderSummary(series) {
84
84
  const rows = this.sourceRows;
@@ -173,6 +173,7 @@ let HelixHeadlessButton = class HelixHeadlessButton extends HelixFormElement {
173
173
  this.watchForm(null);
174
174
  }
175
175
  formAssociatedCallback(form) {
176
+ super.formAssociatedCallback(form);
176
177
  this.watchForm(form);
177
178
  }
178
179
  render() {
@@ -77,7 +77,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
77
77
  aria-describedby=${this.status || this.meaning || this.links ? 'about' : nothing}
78
78
  aria-label=${this.label || nothing}
79
79
  aria-labelledby=${this.label || this.group ? nothing : 'content'}
80
- aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Control+ArrowDown Control+ArrowUp Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete' : nothing}
80
+ aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Control+ArrowDown Control+ArrowUp Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete Plus Minus' : nothing}
81
81
  role=${this.group ? 'group' : 'article'}
82
82
  tabindex=${this.current ? '0' : '-1'}
83
83
  >
@@ -45,7 +45,11 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
45
45
  * double click zooms out.
46
46
  *
47
47
  * Keyboard behavior:
48
- * - One card holds the board's tab stop.
48
+ * - One card holds the board's tab stop, and the board itself leaves the tab
49
+ * order while a card can hold it. Escape on a card goes back out to the
50
+ * board, where the arrow keys pan it.
51
+ * - Plus and minus zoom the board, on the board and on a card or a
52
+ * connection.
49
53
  * - The arrow keys move through the cards in reading order. The card you land
50
54
  * on scrolls into view, and its connections are announced with it.
51
55
  * - The card you land on is selected. Control or Command with an arrow key,
@@ -566,6 +566,11 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
566
566
  }
567
567
  return;
568
568
  }
569
+ if (plain && ['+', '=', '-', '_'].includes(event.key)) {
570
+ event.preventDefault();
571
+ this.zoom(event.key === '+' || event.key === '=' ? 1 : -1);
572
+ return;
573
+ }
569
574
  if (event.key === ' ' && !this.linking) {
570
575
  event.preventDefault();
571
576
  this.toggle(key);
@@ -2143,6 +2148,9 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2143
2148
  const stop = this.stop !== undefined && this.keyNamed(this.stop) === this.stop
2144
2149
  ? this.stop
2145
2150
  : reading(all, this.edges, this.order ?? 'spatial')[0]?.id;
2151
+ const holds = stop !== undefined &&
2152
+ (stop.startsWith('edge:') || this.cards.some((card) => card.name === stop));
2153
+ this.partNamed('region')?.setAttribute('tabindex', holds ? '-1' : '0');
2146
2154
  for (const card of this.cards) {
2147
2155
  const x = finite(card.x) - origin.x;
2148
2156
  const y = finite(card.y) - origin.y;
@@ -2255,7 +2263,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2255
2263
  d=${drawn.d}
2256
2264
  data-edge=${key}
2257
2265
  fill="none"
2258
- aria-keyshortcuts="Enter F2 Space Control+ArrowDown Control+ArrowUp C Delete"
2266
+ aria-keyshortcuts="Enter F2 Space Control+ArrowDown Control+ArrowUp C Delete Plus Minus"
2259
2267
  role="button"
2260
2268
  tabindex=${key === stop ? '0' : '-1'}
2261
2269
  />
@@ -49,7 +49,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
49
  *
50
50
  * @event {HelixSendEvent} helix-send - Fired when the user sends their turn, and cancelable.
51
51
  * @event {HelixTypingEvent} helix-typing - The user started or stopped writing, with `typing` saying which, as the composer's `typing` does.
52
- * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Sent on the composer and does not bubble.
52
+ * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Bubbles and is composed, as a native field's is.
53
53
  *
54
54
  * @csspart composer - The whole composer.
55
55
  * @csspart end - The box around the `end` slot.
@@ -41,7 +41,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
41
41
  event.stopPropagation();
42
42
  this.value = event.target.value;
43
43
  this.edited = true;
44
- this.dispatchEvent(new Event('input'));
44
+ this.announceInput();
45
45
  this.markTyping(this.value !== '');
46
46
  };
47
47
  this.handleSend = () => {
@@ -633,6 +633,16 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
633
633
  * is redrawn though the value it settles on is the one drawn before.
634
634
  */
635
635
  protected renderFields(color: Color): TemplateResult;
636
+ /**
637
+ * A channel's name, after the picker's own where the picker has one.
638
+ *
639
+ * **Every channel the same way**, as React Aria's color area names its own:
640
+ * "Brand, Saturation" says which picker on a page with two. The square's
641
+ * axes and the wheel took the picker's name, and the hue and opacity ramps
642
+ * and the number fields did not, so two pickers on a page gave two sliders
643
+ * named "Hue", measured in Chromium's tree.
644
+ */
645
+ protected channelName(channel: string): string;
636
646
  /**
637
647
  * One of the ramps under the square.
638
648
  *
@@ -407,11 +407,15 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
407
407
  }
408
408
  renderFields(color) {
409
409
  return html `<div part="fields">
410
- ${channelFields(this.value, color, this.format ?? 'hex', this.alpha).map((field) => html `<label part="field">
411
- <span part="field-label">${this.channelWord(field.channel)}</span>
410
+ ${channelFields(this.value, color, this.format ?? 'hex', this.alpha).map((field) => {
411
+ const word = this.channelWord(field.channel);
412
+ const name = this.channelName(word);
413
+ return html `<label part="field">
414
+ <span part="field-label">${word}</span>
412
415
  <input
413
416
  part="field-input"
414
417
  type="number"
418
+ aria-label=${name === word ? nothing : name}
415
419
  inputmode=${field.step < 1 ? 'decimal' : 'numeric'}
416
420
  min=${field.min}
417
421
  max=${field.max}
@@ -421,14 +425,19 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
421
425
  ?readonly=${this.readOnly}
422
426
  @input=${(event) => this.handleField(field.channel, event)}
423
427
  @change=${(event) => {
424
- event.stopPropagation();
425
- this.settle();
426
- this.requestUpdate();
427
- }}
428
+ event.stopPropagation();
429
+ this.settle();
430
+ this.requestUpdate();
431
+ }}
428
432
  />
429
- </label>`)}
433
+ </label>`;
434
+ })}
430
435
  </div>`;
431
436
  }
437
+ channelName(channel) {
438
+ const owner = this.getAttribute('aria-label')?.trim() || this.named;
439
+ return owner ? `${owner}, ${channel}` : channel;
440
+ }
432
441
  renderChannel(channel, label, max, value, spoken = '') {
433
442
  return html `<${this.tag('slider')}
434
443
  part="channel"
@@ -439,7 +448,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
439
448
  tooltips: channel-tooltips"
440
449
  data-channel=${channel}
441
450
  aria-invalid=${this.marked ? 'true' : nothing}
442
- label=${label}
451
+ label=${this.channelName(label)}
443
452
  max=${max}
444
453
  min="0"
445
454
  value-text=${spoken || nothing}
@@ -568,8 +577,6 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
568
577
  const reversed = square('brightness');
569
578
  const hue = hueName({ alpha: 100, brightness: 100, hue: color.hue, saturation: 100 }, this.colorWords, locale);
570
579
  const hueSaid = (value) => new Intl.ListFormat(locale, { style: 'short', type: 'unit' }).format([String(value), hue]);
571
- const owner = this.getAttribute('aria-label')?.trim() || this.named;
572
- const axisName = (channel) => (owner ? `${owner}, ${channel}` : channel);
573
580
  const area = html `<div
574
581
  part="grid"
575
582
  style="--color-picker-hue: ${color.hue}; --color-picker-saturation: ${color.saturation}%; --color-picker-brightness: ${color.brightness}%"
@@ -584,7 +591,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
584
591
  type="range"
585
592
  min="0"
586
593
  max="100"
587
- aria-label=${axisName(this.saturationLabel)}
594
+ aria-label=${this.channelName(this.saturationLabel)}
588
595
  aria-describedby=${this.describedBy}
589
596
  aria-valuetext=${both}
590
597
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -601,7 +608,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
601
608
  max="100"
602
609
  tabindex="-1"
603
610
  style="writing-mode: vertical-lr; direction: rtl"
604
- aria-label=${axisName(this.brightnessLabel)}
611
+ aria-label=${this.channelName(this.brightnessLabel)}
605
612
  aria-describedby=${this.describedBy}
606
613
  aria-valuetext=${reversed}
607
614
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -630,7 +637,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
630
637
  min="0"
631
638
  max="360"
632
639
  dir="ltr"
633
- aria-label=${axisName(this.hueLabel)}
640
+ aria-label=${this.channelName(this.hueLabel)}
634
641
  aria-describedby=${this.describedBy}
635
642
  aria-valuetext=${hueSaid(color.hue)}
636
643
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -395,6 +395,8 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
395
395
  * "France", and the form received two entries.
396
396
  */
397
397
  private get values();
398
+ /** The values a value holds, read as `values` reads this element's own. */
399
+ private valuesIn;
398
400
  /** The answers created here, which no option holds, kept so their commas read as theirs. */
399
401
  private readonly created;
400
402
  /**
@@ -577,6 +579,17 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
577
579
  * `result-label` for one where a page still sets it.
578
580
  */
579
581
  private countSaid;
582
+ /**
583
+ * Says the option a choice changed, for VoiceOver, which says nothing when
584
+ * `aria-selected` changes: `Beta, selected` for one taken, and its words
585
+ * alone for one let go, as the highlight says an option not chosen.
586
+ *
587
+ * **The option the choice changed, read from the values before and after.**
588
+ * With `multiple` the value is the whole list, and the option was looked up
589
+ * by it, so no option matched and no choice was ever said: measured with
590
+ * Beta chosen, Enter let it go and the live region kept `Beta, selected`.
591
+ */
592
+ private sayChoice;
580
593
  /**
581
594
  * Takes what the list announced and stops it there.
582
595
  *
@@ -188,12 +188,12 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
188
188
  this.handleChange = (event) => {
189
189
  event.stopPropagation();
190
190
  const { value } = event.detail;
191
+ const before = this.values;
191
192
  if (!this.requestValue(value)) {
192
193
  return;
193
194
  }
194
- const chosen = this.listbox?.options.find((option) => option.value === value);
195
- if (chosen && onApple()) {
196
- this.status.say(formatWords(this, 'selectedLabel', { name: chosen.text }));
195
+ if (onApple()) {
196
+ this.sayChoice(before, this.valuesIn(value));
197
197
  }
198
198
  this.emptied = false;
199
199
  this.typed = '';
@@ -253,11 +253,14 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
253
253
  return this.partNamed('listbox');
254
254
  }
255
255
  get values() {
256
+ return this.valuesIn(this.value);
257
+ }
258
+ valuesIn(value) {
256
259
  const known = new Set(this.created);
257
260
  for (const option of optionsAmong(this.knownChildren)) {
258
261
  known.add(option.value);
259
262
  }
260
- return chosenValues(this.value, known, this.multiple);
263
+ return chosenValues(value, known, this.multiple);
261
264
  }
262
265
  get chosen() {
263
266
  const options = optionsAmong(this.knownChildren);
@@ -460,6 +463,15 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
460
463
  ? countLabel(count, this.resultLabel, this.resultsLabel, wordsLocale(this))
461
464
  : formatWords(this, 'resultsLabel', { count });
462
465
  }
466
+ sayChoice(before, after) {
467
+ const taken = this.multiple ? after.find((one) => !before.includes(one)) : after[0];
468
+ const changed = taken ?? before.find((one) => !after.includes(one));
469
+ const option = this.listbox?.options.find((one) => one.value === changed);
470
+ if (!option) {
471
+ return;
472
+ }
473
+ this.status.say(taken === undefined ? option.text : formatWords(this, 'selectedLabel', { name: option.text }));
474
+ }
463
475
  removeLastToken() {
464
476
  const last = this.values.at(-1);
465
477
  if (last !== undefined) {
@@ -10,7 +10,7 @@ export interface DonutShare {
10
10
  /** Where the slice starts, as a share of the whole. */
11
11
  start: number;
12
12
  }
13
- declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface);
13
+ declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartStatusInterface);
14
14
  /**
15
15
  * @summary A chart that shows parts of a whole as a ring, read from a table.
16
16
  *
@@ -34,6 +34,27 @@ declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args:
34
34
  * follows, the `live` part says the slice in words, and Escape puts the
35
35
  * tooltip away.
36
36
  *
37
+ * **A tap shows the slice under it**, and keeps it until a tap lands
38
+ * elsewhere. `helix-reading-change` says each move of the slice shown, by
39
+ * pointer, tap or key, and `helix-reading-press` says a slice was clicked,
40
+ * tapped, or pressed with Enter or Space, for a page that opens what a slice
41
+ * stands for. `index` is the slice's row in the table.
42
+ *
43
+ * **A chart with nothing to draw says so.** A table with no readings shows
44
+ * `empty-text`, "No readings", in the plot, and `loading` shows `loading-text`
45
+ * there until readings arrive. The `empty` and `loading` slots take markup in
46
+ * their place.
47
+ *
48
+ * **Keyboard.** The plot is one tab stop.
49
+ *
50
+ * | Key | What it does |
51
+ * |---|---|
52
+ * | Tab | Moves focus to the plot and shows the first slice. |
53
+ * | Any arrow | Right and Down move to the next slice, Left and Up to the previous. On a right-to-left page, Left moves to the next. |
54
+ * | Home, End | Moves to the first or the last slice. |
55
+ * | Enter, Space | Presses the slice shown, which sends `helix-reading-press`. |
56
+ * | Escape | Puts the slice and its tooltip away. |
57
+ *
37
58
  * @since 0.1.0
38
59
  * @status stable
39
60
  * @category charts
@@ -41,16 +62,24 @@ declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args:
41
62
  * @documentation https://fusion.dev/helix/components/donut-chart
42
63
  *
43
64
  * @slot - Content in the middle of the ring, usually a total. It is read by screen readers, while the ring itself is hidden from them.
65
+ * @slot empty - Content shown in place of the ring when the table has no readings. Falls back to `empty-text`.
66
+ * @slot loading - Content shown in place of the ring while `loading` is set and there are no readings yet. Falls back to `loading-text`.
67
+ *
68
+ * @event {HelixReadingChangeEvent} helix-reading-change - The slice shown moved, or was put away: `index` is the slice's row, or `-1` for none, with its `label` and the `pointerType` that moved it.
69
+ * @event {HelixReadingPressEvent} helix-reading-press - A slice was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
44
70
  *
45
71
  * @csspart center - The middle of the ring, holding whatever is slotted.
72
+ * @csspart empty - The words in place of the ring when the table has no readings: the `empty` slot, falling back to `empty-text`. Not drawn while the table cannot be read yet, on the server and during hydration.
46
73
  * @csspart live - The readout: a polite live region that says the slice the keys moved to. Hide it visually.
74
+ * @csspart loading - The words in place of the ring while `loading` is set and there are no readings: the `loading` slot, falling back to `loading-text`.
47
75
  * @csspart mark - The invisible point the tooltip is anchored to.
48
- * @csspart plot - The drawing, which takes focus to be read by key.
76
+ * @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.
49
77
  * @csspart segment - One slice.
50
78
  * @csspart segment-active - The slice under the pointer. Set alongside `segment`.
51
79
  * @csspart segment-edge - One slice as a closed shape, used to draw its outline.
52
80
  * @csspart slice-label - One slice's share, written on the slice under `slice-labels`. Placed with `left` and `top`, or `inset-inline-start` and `top` on a half ring, and carries its series color as `--chart-series`.
53
81
  * @csspart summary - The chart's name and each slice's share, written as a sentence for screen readers. Hide it visually.
82
+ * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
54
83
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
55
84
  * @csspart tooltip-anchor - The `Tooltip` element itself.
56
85
  * @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
@@ -70,6 +99,10 @@ declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args:
70
99
  * @csspart track - The ring the slices are drawn on.
71
100
  *
72
101
  * @cssproperty --donut-chart-stroke - How thick the ring is drawn, derived from `hole`.
102
+ *
103
+ * @cssstate active - While a slice is shown, under the pointer or the keys, with its tooltip open.
104
+ * @cssstate empty - While the `empty` part stands in place of the ring.
105
+ * @cssstate loading - While the `loading` part stands in place of the ring.
73
106
  */
74
107
  export declare class HelixHeadlessDonutChart extends HelixHeadlessDonutChart_base {
75
108
  /**
@@ -204,6 +237,8 @@ export declare class HelixHeadlessDonutChart extends HelixHeadlessDonutChart_bas
204
237
  * The shares, because they are what the ring shows and the table does not:
205
238
  * the table holds the amounts, and the percentages are this chart's
206
239
  * arithmetic. In the ring's order, which is the table's and the legend's.
240
+ * A row the ring leaves out, a negative amount or a slice a legend hid, is
241
+ * left out here too.
207
242
  */
208
243
  protected renderSummary(shares: DonutShare[]): TemplateResult;
209
244
  /**
@@ -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, customElement, drawTooltip, HelixElement, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { CHART_PLOT, customElement, drawTooltip, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, MixinChartSource, MixinChartStatus, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
@@ -30,7 +30,7 @@ const THICKNESS = {
30
30
  none: EDGE,
31
31
  small: 26,
32
32
  };
33
- let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSource(HelixElement) {
33
+ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartStatus(MixinChartSource(HelixElement)) {
34
34
  constructor() {
35
35
  super(...arguments);
36
36
  this.sliceLabels = false;
@@ -39,6 +39,11 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
39
39
  connectedCallback() {
40
40
  super.connectedCallback();
41
41
  this.pointer.axis = 'none';
42
+ this.pointer.onReading = (heard, detail) => {
43
+ const row = this.shares[detail.index]?.row ?? -1;
44
+ const told = { ...detail, index: row, label: this.sourceRows[row]?.label ?? '' };
45
+ this.dispatchEvent(heard === 'press' ? new HelixReadingPressEvent(told) : new HelixReadingChangeEvent(told));
46
+ };
42
47
  }
43
48
  get span() {
44
49
  return this.sweep === 'half' ? 0.5 : 1;
@@ -81,28 +86,32 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
81
86
  }
82
87
  render() {
83
88
  const shares = this.shares;
84
- return html `${this.renderPlot(shares)}${shares.length === 0 ? nothing : this.renderSummary(shares)}`;
89
+ if (shares.length === 0 && (this.readingsKnown || this.loading)) {
90
+ return this.renderEmpty();
91
+ }
92
+ return html `${this.renderPlot(shares)}${shares.length === 0 ? nothing : this.renderSummary(shares)}${this.renderTable()}`;
85
93
  }
86
94
  renderSummary(shares) {
87
95
  const rows = this.sourceRows;
88
96
  const locale = wordsLocale(this);
89
97
  const parts = shares
90
- .filter((share) => this.showsSeries(rows[share.row]?.label ?? ''))
98
+ .filter((share) => share.fraction > 0)
91
99
  .map((share) => `${rows[share.row]?.label ?? ''} ${percentIn(locale).format(share.fraction)}`);
92
100
  const named = this.named ? `${this.named}: ` : '';
93
101
  const listed = new Intl.ListFormat(locale, { style: 'short', type: 'unit' }).format(parts);
94
102
  return html `<p id="helix-chart-summary" part="summary">${named}${listed}.</p>`;
95
103
  }
96
104
  renderPlot(shares) {
105
+ const drawn = shares.length > 0;
97
106
  return html `<div
98
- aria-describedby=${this.named ? `helix-chart-summary${this.hole === 'none' ? '' : ' helix-chart-center'}` : nothing}
107
+ aria-describedby=${drawn && this.named ? `helix-chart-summary${this.hole === 'none' ? '' : ' helix-chart-center'}` : nothing}
99
108
  aria-label=${this.named || nothing}
100
- aria-labelledby=${this.named ? nothing : 'helix-chart-summary'}
109
+ aria-labelledby=${drawn && !this.named ? 'helix-chart-summary' : nothing}
101
110
  id=${CHART_PLOT}
102
111
  part="plot"
103
- role="application"
112
+ role=${drawn ? 'application' : this.named ? 'group' : nothing}
104
113
  style="--donut-chart-stroke: ${this.thickness}"
105
- tabindex="0"
114
+ tabindex=${drawn ? '0' : nothing}
106
115
  >
107
116
  <svg
108
117
  aria-hidden="true"
@@ -175,6 +184,11 @@ let HelixHeadlessDonutChart = class HelixHeadlessDonutChart extends MixinChartSo
175
184
  this.pointer.set(index);
176
185
  }
177
186
  }}
187
+ @pointerup=${(event) => {
188
+ if (event.pointerType === 'touch') {
189
+ this.pointer.tapTo(index);
190
+ }
191
+ }}
178
192
  part="segment${active ? ' segment-active' : ''}"
179
193
  r=${this.radius}
180
194
  stroke-dasharray="${length} ${GAP}"
@@ -108,6 +108,7 @@ registerElementEvents({
108
108
  ],
109
109
  'helix-headless-date-picker': ['change', 'helix-before-change', 'helix-navigate', 'input'],
110
110
  'helix-headless-dialog': ['close', 'helix-before-close', 'helix-before-open', 'toggle'],
111
+ 'helix-headless-donut-chart': ['helix-reading-change', 'helix-reading-press'],
111
112
  'helix-headless-drawer': ['close', 'helix-before-close', 'helix-before-open', 'toggle'],
112
113
  'helix-headless-drilldown': ['helix-navigate', 'helix-select'],
113
114
  'helix-headless-drilldown-item': ['helix-select'],
@@ -249,6 +250,7 @@ registerElementEvents({
249
250
  'toggle',
250
251
  ],
251
252
  'helix-headless-slider': ['change', 'helix-before-change', 'input'],
253
+ 'helix-headless-sparkline': ['helix-reading-change', 'helix-reading-press'],
252
254
  'helix-headless-speed-dial': [
253
255
  'helix-before-close',
254
256
  'helix-before-open',
@@ -360,8 +360,9 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
360
360
  *
361
361
  * Not through `commit`, and that is the point of the split: a reset is the
362
362
  * form's decision and there is nothing here for a listener to refuse. What it
363
- * owes is the report and the platform's own announcement, which is what
364
- * `settle` is.
363
+ * owes is the report to the form, and no `input` or `change`: a native file
364
+ * input sends neither on `form.reset()`, measured in Chromium, where this
365
+ * sent both.
365
366
  */
366
367
  formResetCallback(): void;
367
368
  /**
@@ -224,10 +224,13 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
224
224
  }
225
225
  control.files = holding.files;
226
226
  }
227
- settle() {
227
+ settle(announce = true) {
228
228
  this.committed = this.files;
229
229
  this.sync();
230
230
  this.requestUpdate();
231
+ if (!announce) {
232
+ return;
233
+ }
231
234
  this.announceInput();
232
235
  this.announceChange();
233
236
  }
@@ -265,7 +268,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
265
268
  if (this.control) {
266
269
  restoreValue(this.control, '');
267
270
  }
268
- this.settle();
271
+ this.settle(false);
269
272
  }
270
273
  renderMeta(file) {
271
274
  return html `<${this.tag('tag')} part="size"