@fusion.dev/helix-headless 0.150.0 → 0.151.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 (40) hide show
  1. package/custom-elements.json +741 -66
  2. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  3. package/dist/area-chart/root/area-chart-root.js +17 -2
  4. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  5. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  6. package/dist/callout/root/callout-root.d.ts +20 -0
  7. package/dist/callout/root/callout-root.js +19 -4
  8. package/dist/chat/composer/chat-composer.d.ts +30 -0
  9. package/dist/chat/composer/chat-composer.js +47 -2
  10. package/dist/chat/root/chat-root.d.ts +10 -0
  11. package/dist/chat/root/chat-root.js +14 -0
  12. package/dist/data-table/root/data-table-root.d.ts +34 -2
  13. package/dist/data-table/root/data-table-root.js +63 -2
  14. package/dist/divider/root/divider-root.d.ts +8 -6
  15. package/dist/divider/root/divider-root.js +10 -2
  16. package/dist/element-events.generated.js +4 -0
  17. package/dist/kbd/root/kbd-root.d.ts +7 -1
  18. package/dist/kbd/root/kbd-root.js +8 -4
  19. package/dist/line-chart/root/line-chart-root.d.ts +36 -0
  20. package/dist/line-chart/root/line-chart-root.js +15 -2
  21. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  22. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  23. package/dist/table/column/table-column.js +3 -2
  24. package/dist/table/grid-controller.d.ts +14 -2
  25. package/dist/table/grid-controller.js +27 -1
  26. package/dist/table/root/table-root.d.ts +30 -4
  27. package/dist/table/root/table-root.js +45 -7
  28. package/dist/table/row/table-row.d.ts +12 -2
  29. package/dist/table/row/table-row.js +8 -2
  30. package/dist/video/root/video-root.d.ts +6 -0
  31. package/dist/video/root/video-root.js +5 -1
  32. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  33. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  34. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  35. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  36. package/lint.json +1 -1
  37. package/package.json +5 -5
  38. package/types/jsx.d.ts +648 -98
  39. package/types/vue.d.ts +195 -43
  40. package/types/words.d.ts +4 -2
@@ -52,6 +52,9 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
52
52
  * compare the parts with each other; the scale then fits the largest part
53
53
  * rather than the total.
54
54
  *
55
+ * `value-axis` draws the scale's numbers beside the plot, at the levels
56
+ * `gridlines` rules: amounts in `locale`, or percentages under `share`.
57
+ *
55
58
  * The drawing is hidden from screen readers. The text summary gives the latest
56
59
  * reading's total and each part's share of it, and the table beside the chart
57
60
  * has every number.
@@ -65,13 +68,22 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
65
68
  * Every numeric column is a part, so leave a total column out of the table the
66
69
  * chart reads, or it is counted twice. A negative reading is drawn as zero.
67
70
  *
71
+ * **A tap shows the reading under it**, and keeps it until a tap lands
72
+ * elsewhere. `helix-reading-change` says each move of the reading, by pointer,
73
+ * tap or key, and `helix-reading-press` says a reading was clicked, tapped, or
74
+ * pressed with Enter or Space, for a page that opens what a reading stands for.
75
+ *
68
76
  * @since 0.1.0
69
77
  * @status stable
70
78
  * @category charts
71
79
  * @aliases Stacked Area Chart, Composition Chart, Banded Chart
72
80
  * @documentation https://fusion.dev/helix/components/area-chart
73
81
  *
82
+ * @event {HelixReadingChangeEvent} helix-reading-change - The reading shown moved, or was put away: `index` is the row, or `-1` for none, with its `label`, the `series` the keys rest on, and the `pointerType` that moved it.
83
+ * @event {HelixReadingPressEvent} helix-reading-press - A reading was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
84
+ *
74
85
  * @csspart area - One part's band.
86
+ * @csspart axis - The numbers of the scale beside the plot, under `value-axis`.
75
87
  * @csspart baseline - The rule at zero, which is the floor of the drawing.
76
88
  * @csspart gridline - One rule across the plot, under `gridlines`.
77
89
  * @csspart guide - A rule down the reading a pointer is resting on.
@@ -83,6 +95,7 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
83
95
  * @csspart plot - The drawing, which takes focus to be read by key.
84
96
  * @csspart summary - The total and each part's share at the latest reading, written as a sentence for screen readers.
85
97
  * @csspart svg - The drawing surface the bands sit on.
98
+ * @csspart tick - One number on the axis, placed with `inset-block-start` at its level.
86
99
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
87
100
  * @csspart tooltip-anchor - The `Tooltip` element itself.
88
101
  * @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
@@ -99,6 +112,8 @@ declare const HelixHeadlessAreaChart_base: typeof HelixElement & (new (...args:
99
112
  * @csspart tooltip-right - The bubble while it is right of the reading, beside `tooltip`.
100
113
  * @csspart tooltip-top - The bubble while it is above the reading, beside `tooltip`.
101
114
  * @csspart tooltip-value - A column's value inside the tooltip.
115
+ *
116
+ * @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.
102
117
  */
103
118
  export declare class HelixHeadlessAreaChart extends HelixHeadlessAreaChart_base {
104
119
  /**
@@ -106,6 +121,13 @@ export declare class HelixHeadlessAreaChart extends HelixHeadlessAreaChart_base
106
121
  * `registerWords('de', { 'helix-area-chart': { totalLabel: 'Summe' } })`.
107
122
  */
108
123
  static words: string;
124
+ /**
125
+ * Sends the reading's moves and presses as `helix-reading-change` and
126
+ * `helix-reading-press`, which the pointer hears. Here and not in a
127
+ * constructor, where the manifest's analyzer reads the assignment as a
128
+ * member of this class.
129
+ */
130
+ connectedCallback(): void;
109
131
  /**
110
132
  * How a band's edge gets from one reading to the next.
111
133
  *
@@ -161,6 +183,14 @@ export declare class HelixHeadlessAreaChart extends HelixHeadlessAreaChart_base
161
183
  * The name for the sum of the parts, shown in the tooltip and the summary.
162
184
  */
163
185
  totalLabel: string;
186
+ /**
187
+ * Draws the scale's numbers beside the plot, the `axis` part, at the levels
188
+ * `gridlines` rules: amounts, or percentages under `share`.
189
+ *
190
+ * Absent draws none: bands read as a composition need no numbers beside
191
+ * them, and the tooltip and the table hold every reading.
192
+ */
193
+ valueAxis: boolean;
164
194
  private warnedAboutNegatives;
165
195
  /** Whether the levels are ruled, which is every value but `category`. */
166
196
  protected get rulesValue(): boolean;
@@ -184,6 +214,12 @@ export declare class HelixHeadlessAreaChart extends HelixHeadlessAreaChart_base
184
214
  */
185
215
  private warnNegative;
186
216
  protected render(): TemplateResult;
217
+ /**
218
+ * The scale's numbers beside the plot, under `value-axis`: amounts written
219
+ * in the chart's locale, or percentages under `share`. A seam, so a
220
+ * subclass can write the numbers its own way, such as a currency.
221
+ */
222
+ protected renderAxis(y: (value: number) => number, domain: [number, number]): TemplateResult;
187
223
  /**
188
224
  * The bands, behind a seam the styled tier can move.
189
225
  *
@@ -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, formatNumbers, formatWords, HelixElement, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, reportRuntime, seriesIndex, ticks, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { CHART_PLOT, customElement, drawTooltip, drawValueAxis, formatNumbers, formatWords, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, reportRuntime, seriesIndex, ticks, words, 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';
@@ -27,9 +27,16 @@ let HelixHeadlessAreaChart = class HelixHeadlessAreaChart extends MixinChartSour
27
27
  this.share = false;
28
28
  this.summaryLabel = '{reading}: {totalLabel} {total}; {shares}.';
29
29
  this.totalLabel = 'Total';
30
+ this.valueAxis = false;
30
31
  this.warnedAboutNegatives = false;
31
32
  }
32
33
  static { this.words = 'helix-area-chart'; }
34
+ connectedCallback() {
35
+ super.connectedCallback();
36
+ this.pointer.onReading = (heard, detail) => this.dispatchEvent(heard === 'press'
37
+ ? new HelixReadingPressEvent(detail)
38
+ : new HelixReadingChangeEvent(detail));
39
+ }
33
40
  get rulesValue() {
34
41
  return this.gridlines !== undefined && this.gridlines !== 'category';
35
42
  }
@@ -100,9 +107,14 @@ let HelixHeadlessAreaChart = class HelixHeadlessAreaChart extends MixinChartSour
100
107
  const y = linear(domain, [PLOT_HEIGHT, 0]);
101
108
  const slot = PLOT_WIDTH / rows.length;
102
109
  const x = linear([0, Math.max(rows.length - 1, 1)], [slot / 2, PLOT_WIDTH - slot / 2]);
103
- return html `${this.renderPlot(stack, rows.length, x, y, domain)}
110
+ return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(stack, rows.length, x, y, domain)}
104
111
  ${this.renderLabels(rows.map((row) => row.label))} ${this.renderSummary(stack, rows)}`;
105
112
  }
113
+ renderAxis(y, domain) {
114
+ const locale = wordsLocale(this);
115
+ const percent = percentIn(locale);
116
+ return drawValueAxis(domain, (value) => (y(value) / PLOT_HEIGHT) * 100, this.share ? (value) => percent.format(value) : formatNumbers(locale), (widest) => `--area-chart-axis: ${widest}ch`);
117
+ }
106
118
  renderPlot(stack, count, x, y, domain) {
107
119
  const order = stack.layers.map((_, depth) => depth).reverse();
108
120
  return html `<div
@@ -249,6 +261,9 @@ __decorate([
249
261
  words,
250
262
  property({ attribute: 'total-label' })
251
263
  ], HelixHeadlessAreaChart.prototype, "totalLabel", void 0);
264
+ __decorate([
265
+ property({ attribute: 'value-axis', reflect: true, type: Boolean })
266
+ ], HelixHeadlessAreaChart.prototype, "valueAxis", void 0);
252
267
  HelixHeadlessAreaChart = __decorate([
253
268
  customElement('helix-headless-area-chart')
254
269
  ], HelixHeadlessAreaChart);
@@ -30,13 +30,21 @@ declare const HelixHeadlessBarChart_base: typeof HelixElement & (new (...args: a
30
30
  * The component writes the custom properties below with numbers it computes,
31
31
  * and the stylesheet that draws the chart reads them. Do not set them yourself.
32
32
  *
33
+ * **A tap shows the reading under it**, and keeps it until a tap lands
34
+ * elsewhere. `helix-reading-change` says each move of the reading, by pointer,
35
+ * tap or key, and `helix-reading-press` says a reading was clicked, tapped, or
36
+ * pressed with Enter or Space, for a page that opens what a reading stands for.
37
+ *
33
38
  * @since 0.1.0
34
39
  * @status stable
35
40
  * @category charts
36
41
  * @aliases Column Chart, Categorical Chart
37
42
  * @documentation https://fusion.dev/helix/components/bar-chart
38
43
  *
39
- * @csspart axis - The numbers of one scale, drawn only when `secondary` splits the chart into two scales.
44
+ * @event {HelixReadingChangeEvent} helix-reading-change - The reading shown moved, or was put away: `index` is the row, or `-1` for none, with its `label`, the `series` the keys rest on, and the `pointerType` that moved it.
45
+ * @event {HelixReadingPressEvent} helix-reading-press - A reading was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
46
+ *
47
+ * @csspart axis - The numbers of one scale: the primary under `value-axis`, and both when `secondary` splits the chart into two scales.
40
48
  * @csspart band - The category band under the pointer.
41
49
  * @csspart bar - One column.
42
50
  * @csspart bar-wash-end - The far stop of a bar's gradient, where one is used.
@@ -83,6 +91,13 @@ export declare class HelixHeadlessBarChart extends HelixHeadlessBarChart_base {
83
91
  * `registerWords('de', { 'helix-bar-chart': { highestLabel: 'Höchster Wert' } })`.
84
92
  */
85
93
  static words: string;
94
+ /**
95
+ * Sends the reading's moves and presses as `helix-reading-change` and
96
+ * `helix-reading-press`, which the pointer hears. Here and not in a
97
+ * constructor, where the manifest's analyzer reads the assignment as a
98
+ * member of this class.
99
+ */
100
+ connectedCallback(): void;
86
101
  /**
87
102
  * Which gridlines are drawn across the plot, if any.
88
103
  *
@@ -199,6 +214,15 @@ export declare class HelixHeadlessBarChart extends HelixHeadlessBarChart_base {
199
214
  * the reading as the table writes it.
200
215
  */
201
216
  summaryLabel: string;
217
+ /**
218
+ * Draws the numbers of the scale the bars are measured on, the `axis` part,
219
+ * at the levels `gridlines` rules: down the side of columns, and along the
220
+ * bottom of bars under `orientation="horizontal"`.
221
+ *
222
+ * Absent draws none with one scale, since bars compared with each other need
223
+ * no numbers. With `secondary` both scales are drawn whatever this says.
224
+ */
225
+ valueAxis: boolean;
202
226
  /** The series names on the second scale, lowercased and trimmed. */
203
227
  private get scaled();
204
228
  /** The series names drawn as a line, lowercased and trimmed for matching. */
@@ -251,8 +275,8 @@ export declare class HelixHeadlessBarChart extends HelixHeadlessBarChart_base {
251
275
  * a small chart is furniture that makes the drawing smaller for no reading a
252
276
  * reader was going to do. With two scales that argument inverts: a reader
253
277
  * cannot tell there are two, cannot tell which series is on which, and cannot
254
- * recover either from the picture. So this appears with `secondary` and never
255
- * without it.
278
+ * recover either from the picture. So this appears with `secondary`, and
279
+ * with one scale only where `value-axis` asks for its numbers.
256
280
  *
257
281
  * **Beside the plot and not inside it.** Inside, the numbers are drawn over
258
282
  * the drawing, and a horizontal chart shows it immediately: its value axis
@@ -263,7 +287,7 @@ export declare class HelixHeadlessBarChart extends HelixHeadlessBarChart_base {
263
287
  * to the right.
264
288
  *
265
289
  * A column of the host grid instead, which the stylesheet grows only when
266
- * `secondary` is set. That is what the property reflects for.
290
+ * `secondary` or `value-axis` is set. That is what the two reflect for.
267
291
  *
268
292
  * The primary hugs the edge the drawing starts at and the secondary the edge
269
293
  * it ends at: down the two sides for columns, along the bottom and top for
@@ -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 { byRow, CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, formatWords, HelixElement, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesKey, seriesNames, ticks, unbroken, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { byRow, CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, formatWords, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesKey, seriesNames, ticks, unbroken, words, 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 { stackEnds, stackedReadings, stackSpans } from '../stack.js';
@@ -15,8 +15,15 @@ let HelixHeadlessBarChart = class HelixHeadlessBarChart extends MixinChartSource
15
15
  this.highestLabel = 'Highest';
16
16
  this.stacked = false;
17
17
  this.summaryLabel = '{highestLabel}: {readings}.';
18
+ this.valueAxis = false;
18
19
  }
19
20
  static { this.words = 'helix-bar-chart'; }
21
+ connectedCallback() {
22
+ super.connectedCallback();
23
+ this.pointer.onReading = (heard, detail) => this.dispatchEvent(heard === 'press'
24
+ ? new HelixReadingPressEvent(detail)
25
+ : new HelixReadingChangeEvent(detail));
26
+ }
20
27
  get scaled() {
21
28
  return new Set(seriesNames(this.secondary));
22
29
  }
@@ -61,14 +68,17 @@ let HelixHeadlessBarChart = class HelixHeadlessBarChart extends MixinChartSource
61
68
  const other = [Math.min(0, second), Math.max(0, top)];
62
69
  const z = scaled.size > 0 ? linear(other, range) : undefined;
63
70
  const format = formatNumbers(wordsLocale(this));
64
- const widest = z === undefined
71
+ const levels = [...ticks(domain), ...(z === undefined ? [] : ticks(other))];
72
+ const widest = z === undefined && !this.valueAxis
65
73
  ? 0
66
- : Math.max(...[...ticks(domain), ...ticks(other)].map((level) => format(level).length));
74
+ : Math.max(1, ...levels.map((level) => format(level).length));
67
75
  const lone = series.filter(own);
68
76
  const tint = lone.length === 1 && lone[0] !== undefined
69
77
  ? `--bar-chart-secondary: var(--chart-series-${seriesIndex(series.indexOf(lone[0]))})`
70
78
  : undefined;
71
- return html `${z === undefined ? nothing : 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)}`;
79
+ return html `${z === undefined && !this.valueAxis
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)}`;
72
82
  }
73
83
  renderSummary(series) {
74
84
  const rows = this.sourceRows;
@@ -309,6 +319,9 @@ __decorate([
309
319
  words,
310
320
  property({ attribute: 'summary-label' })
311
321
  ], HelixHeadlessBarChart.prototype, "summaryLabel", void 0);
322
+ __decorate([
323
+ property({ attribute: 'value-axis', reflect: true, type: Boolean })
324
+ ], HelixHeadlessBarChart.prototype, "valueAxis", void 0);
312
325
  HelixHeadlessBarChart = __decorate([
313
326
  customElement('helix-headless-bar-chart')
314
327
  ], HelixHeadlessBarChart);
@@ -48,6 +48,16 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
48
48
  */
49
49
  static words: string;
50
50
  protected willUpdate(changed: PropertyValues): void;
51
+ /**
52
+ * The dismiss button's name, read by screen readers, with `{label}` for
53
+ * what the callout is called: its heading, or the tone's name where it has
54
+ * none.
55
+ *
56
+ * "Dismiss Scheduled maintenance" says which of several callouts the button
57
+ * takes away, where every button was "Dismiss". A label without `{label}` is
58
+ * read as written.
59
+ */
60
+ dismissLabel: string;
51
61
  /**
52
62
  * What kind of attention the passage needs.
53
63
  *
@@ -96,6 +106,16 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
96
106
  * above.
97
107
  */
98
108
  protected renderDismissIcon(): TemplateResult;
109
+ /**
110
+ * The dismiss button's name: `dismissLabel` with the heading's words, or the
111
+ * tone's name.
112
+ *
113
+ * The heading is read from the children, so a server and a hydrating render
114
+ * name the button for the tone, and the update after names it for the
115
+ * heading.
116
+ */
117
+ private dismissName;
118
+ protected firstUpdated(changed: PropertyValues): void;
99
119
  protected render(): TemplateResult;
100
120
  }
101
121
  declare global {
@@ -4,13 +4,14 @@ 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, drawDismiss, drawDismissGlyph, HelixElement, hostAria, MixinDismissible, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
7
+ import { customElement, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
11
  let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(HelixElement) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.dismissLabel = 'Dismiss {label}';
14
15
  this.toneLabel = '';
15
16
  this.toneLabels = {
16
17
  danger: 'Error',
@@ -26,7 +27,7 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
26
27
  hostAria(this.internals, { role: 'note' });
27
28
  }
28
29
  renderIcon(icon, _label) {
29
- return html `<slot name="icon">
30
+ return html `<slot name="icon" aria-hidden="true">
30
31
  <svg part="icon-mark" fill="currentColor" viewBox=${icon.viewBox} aria-hidden="true">
31
32
  ${unsafeSVG(icon.content)}
32
33
  </svg>
@@ -37,23 +38,37 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
37
38
  ${drawDismissGlyph()}
38
39
  </slot>`;
39
40
  }
41
+ dismissName(tone) {
42
+ const heading = this.childrenKnown ? slottedWords(this, ['heading']) : '';
43
+ return this.dismissLabel.includes('{label}')
44
+ ? formatWords(this, 'dismissLabel', { label: heading || tone }).trim()
45
+ : this.dismissLabel;
46
+ }
47
+ firstUpdated(changed) {
48
+ super.firstUpdated(changed);
49
+ this.finishHydration();
50
+ }
40
51
  render() {
41
52
  const tone = this.tone === 'neutral' ? 'neutral' : toneMark(this.tone) ? (this.tone ?? 'info') : 'info';
42
53
  const { icon } = toneMark(tone) ?? TONE_MARKS.info;
43
54
  const named = this.toneLabel || (this.toneLabels[tone] ?? '');
44
55
  return html `<span part="icon" role="img" aria-label=${named}>${this.renderIcon(icon, named)}</span>
45
56
  <div part="content">
46
- <slot name="heading"></slot>
57
+ <slot name="heading" @slotchange=${() => this.requestUpdate()}></slot>
47
58
  <slot></slot>
48
59
  </div>${this.dismissible
49
60
  ? drawDismiss({
50
- label: this.dismissLabel,
61
+ label: this.dismissName(named),
51
62
  mark: this.renderDismissIcon(),
52
63
  onDismiss: () => this.dismiss(),
53
64
  })
54
65
  : nothing}`;
55
66
  }
56
67
  };
68
+ __decorate([
69
+ words,
70
+ property({ attribute: 'dismiss-label' })
71
+ ], HelixHeadlessCallout.prototype, "dismissLabel", void 0);
57
72
  __decorate([
58
73
  property({ reflect: true })
59
74
  ], HelixHeadlessCallout.prototype, "tone", void 0);
@@ -20,6 +20,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
20
20
  * disabled>` turns the composer off. Sending is still `helix-send`, and it
21
21
  * does not submit the form.
22
22
  *
23
+ * **`required`, `minlength` and `maxlength` are checked by the browser**, as
24
+ * on a `<textarea>`, and the message is the browser's. `maxlength` stops the
25
+ * field taking more. A draft that fails them does not send, and the composer
26
+ * takes `:state(invalid)` and `aria-invalid` once the reader has tried to send
27
+ * it, the form was submitted, or the reader edited the field and left it.
28
+ * Tabbing past an empty composer marks nothing.
29
+ *
23
30
  * **`typing` says whether the user is writing**, and `helix-typing` is sent
24
31
  * each time it changes: when they start, and when they send or stop for
25
32
  * `typing-timeout` milliseconds. Pass it on to show "is typing" to the other
@@ -45,6 +52,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
45
52
  * @csspart send - The send button.
46
53
  * @csspart start - The box around the `start` slot.
47
54
  * @csspart tools - The row under the field.
55
+ *
56
+ * @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it.
48
57
  */
49
58
  export declare class HelixHeadlessChatComposer extends HelixFormElement {
50
59
  /**
@@ -56,10 +65,24 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
56
65
  * The field's name, read by screen readers.
57
66
  */
58
67
  label: string;
68
+ /**
69
+ * The most characters a draft can have. The field stops taking more.
70
+ */
71
+ maxLength?: number;
72
+ /**
73
+ * The fewest characters a draft can have, once the reader has written in
74
+ * the field, as on a `<textarea>`.
75
+ */
76
+ minLength?: number;
59
77
  /**
60
78
  * Placeholder text shown in an empty field.
61
79
  */
62
80
  placeholder: string;
81
+ /**
82
+ * Whether the form needs a draft to submit. An empty draft never sends,
83
+ * required or not.
84
+ */
85
+ required: boolean;
63
86
  /**
64
87
  * How many rows the field opens at.
65
88
  *
@@ -97,6 +120,10 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
97
120
  readonly isChatComposer = true;
98
121
  /** Whether the user is writing, which `helix-typing` announces. */
99
122
  private writing;
123
+ /** Whether the reader has tried to send or submit the draft, or edited it and left. */
124
+ private touched;
125
+ /** Whether the composer shows that its draft is wrong. */
126
+ private shownInvalid;
100
127
  /** The pause after which the user has stopped writing. */
101
128
  private readonly pause;
102
129
  /**
@@ -121,6 +148,8 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
121
148
  private get declared();
122
149
  /** Puts back the draft the markup declared when the form is reset. */
123
150
  formResetCallback(): void;
151
+ /** The reader has had a go at the draft, so whether it is wrong can be shown. */
152
+ private readonly touch;
124
153
  /** Brings back the draft when the browser restores the form. */
125
154
  formStateRestoreCallback(state: string | File | FormData | null): void;
126
155
  /**
@@ -133,6 +162,7 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
133
162
  */
134
163
  private markEmpty;
135
164
  protected updated(changed: PropertyValues): void;
165
+ protected firstUpdated(changed: PropertyValues): void;
136
166
  protected willUpdate(changed: PropertyValues): void;
137
167
  /**
138
168
  * The control that offers the turn, behind a seam so a tier can draw its own.
@@ -6,8 +6,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, markRegion, words, } from '@fusion.dev/helix-core';
8
8
  import { arrowElbowDownLeft } from '@fusion.dev/helix-icons';
9
- import { html } from 'lit';
10
- import { property } from 'lit/decorators.js';
9
+ import { html, nothing } from 'lit';
10
+ import { property, state } from 'lit/decorators.js';
11
11
  import { live } from 'lit/directives/live.js';
12
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
13
  let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
@@ -15,6 +15,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
15
15
  super(...arguments);
16
16
  this.label = 'Message';
17
17
  this.placeholder = '';
18
+ this.required = false;
18
19
  this.rows = 1;
19
20
  this.sendLabel = 'Send';
20
21
  this.submitOn = 'enter';
@@ -22,7 +23,13 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
22
23
  this.value = '';
23
24
  this.isChatComposer = true;
24
25
  this.writing = false;
26
+ this.touched = false;
27
+ this.shownInvalid = false;
25
28
  this.pause = new DelayController(this, { renders: false });
29
+ this.touch = () => {
30
+ this.touched = true;
31
+ this.requestUpdate();
32
+ };
26
33
  this.handleInput = (event) => {
27
34
  event.stopPropagation();
28
35
  this.value = event.target.value;
@@ -65,10 +72,15 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
65
72
  if (value === '' || this.isDisabled) {
66
73
  return false;
67
74
  }
75
+ if (!this.internals.validity.valid) {
76
+ this.touch();
77
+ return false;
78
+ }
68
79
  if (!this.dispatchEvent(new HelixSendEvent({ value }))) {
69
80
  return false;
70
81
  }
71
82
  this.value = '';
83
+ this.touched = false;
72
84
  this.markTyping(false);
73
85
  return true;
74
86
  }
@@ -78,6 +90,8 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
78
90
  formResetCallback() {
79
91
  super.formResetCallback();
80
92
  this.value = this.declared;
93
+ this.touched = false;
94
+ this.requestUpdate();
81
95
  }
82
96
  formStateRestoreCallback(state) {
83
97
  this.value = typeof state === 'string' ? state : this.declared;
@@ -93,6 +107,20 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
93
107
  if (changed.has('value')) {
94
108
  this.internals.setFormValue(this.value === '' ? null : this.value);
95
109
  }
110
+ const field = this.partNamed('field');
111
+ if (field?.validity) {
112
+ this.internals.setValidity(field.validity, field.validationMessage, field);
113
+ }
114
+ const invalid = this.touched && !this.internals.validity.valid;
115
+ this.toggleState('invalid', invalid);
116
+ this.internals.ariaInvalid = invalid ? 'true' : null;
117
+ if (invalid !== this.shownInvalid) {
118
+ this.shownInvalid = invalid;
119
+ }
120
+ }
121
+ firstUpdated(changed) {
122
+ super.firstUpdated(changed);
123
+ this.addEventListener('invalid', this.touch);
96
124
  }
97
125
  willUpdate(changed) {
98
126
  super.willUpdate(changed);
@@ -124,7 +152,12 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
124
152
  aria-label=${this.label}
125
153
  placeholder=${this.placeholder}
126
154
  rows=${this.rows}
155
+ ?required=${this.required}
156
+ minlength=${this.minLength ?? nothing}
157
+ maxlength=${this.maxLength ?? nothing}
158
+ aria-invalid=${this.shownInvalid ? 'true' : nothing}
127
159
  ?disabled=${this.isDisabled}
160
+ @change=${this.touch}
128
161
  @input=${this.handleInput}
129
162
  @keydown=${this.handleKey}
130
163
  ></textarea>
@@ -140,9 +173,18 @@ __decorate([
140
173
  words,
141
174
  property()
142
175
  ], HelixHeadlessChatComposer.prototype, "label", void 0);
176
+ __decorate([
177
+ property({ attribute: 'maxlength', type: Number })
178
+ ], HelixHeadlessChatComposer.prototype, "maxLength", void 0);
179
+ __decorate([
180
+ property({ attribute: 'minlength', type: Number })
181
+ ], HelixHeadlessChatComposer.prototype, "minLength", void 0);
143
182
  __decorate([
144
183
  property()
145
184
  ], HelixHeadlessChatComposer.prototype, "placeholder", void 0);
185
+ __decorate([
186
+ property({ type: Boolean })
187
+ ], HelixHeadlessChatComposer.prototype, "required", void 0);
146
188
  __decorate([
147
189
  property({ type: Number })
148
190
  ], HelixHeadlessChatComposer.prototype, "rows", void 0);
@@ -159,6 +201,9 @@ __decorate([
159
201
  __decorate([
160
202
  property()
161
203
  ], HelixHeadlessChatComposer.prototype, "value", void 0);
204
+ __decorate([
205
+ state()
206
+ ], HelixHeadlessChatComposer.prototype, "shownInvalid", void 0);
162
207
  HelixHeadlessChatComposer = __decorate([
163
208
  customElement('helix-headless-chat-composer')
164
209
  ], HelixHeadlessChatComposer);
@@ -331,6 +331,16 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
331
331
  get messages(): HelixHeadlessChatMessage[];
332
332
  /** The element that scrolls, once there is one. */
333
333
  private get log();
334
+ /**
335
+ * Makes the log a tab stop while it scrolls, and takes the stop away when it
336
+ * does not.
337
+ *
338
+ * **A reader on a keyboard scrolls the history from the log itself.**
339
+ * Chromium and Firefox make a box that scrolls focusable on their own, and
340
+ * Safari does not, so there the arrow keys could not reach a long history.
341
+ * A log that fits is no stop at all, so Tab goes straight to the composer.
342
+ */
343
+ private markScroller;
334
344
  /** Whether the reader is within `threshold` of the bottom right now. */
335
345
  private get atBottom();
336
346
  /** What identifies a turn fed as data: its key, or the object it is. */
@@ -78,6 +78,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
78
78
  });
79
79
  this.filled = undefined;
80
80
  this.handleResize = () => {
81
+ this.markScroller();
81
82
  if (this.pinned) {
82
83
  this.stick();
83
84
  }
@@ -141,6 +142,18 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
141
142
  get log() {
142
143
  return this.partNamed('log');
143
144
  }
145
+ markScroller() {
146
+ const log = this.log;
147
+ if (!log) {
148
+ return;
149
+ }
150
+ if (log.scrollHeight > log.clientHeight) {
151
+ log.tabIndex = 0;
152
+ }
153
+ else {
154
+ log.removeAttribute('tabindex');
155
+ }
156
+ }
144
157
  get atBottom() {
145
158
  const log = this.log;
146
159
  if (!log) {
@@ -421,6 +434,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
421
434
  this.virtualize.measure();
422
435
  }
423
436
  this.watchSizes();
437
+ this.markScroller();
424
438
  this.remember();
425
439
  this.reachHistory();
426
440
  this.announce();