@fusion.dev/helix-headless 0.177.2 → 0.179.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.
@@ -2,7 +2,7 @@ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../tooltip/index.js';
4
4
  import { type Reading } from './buckets.js';
5
- declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface);
5
+ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartStatusInterface);
6
6
  /**
7
7
  * @summary A strip of cells, each colored by its reading, for uptime, contributions, or activity.
8
8
  *
@@ -50,6 +50,25 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
50
50
  * `helix-before-change` and tells with `change`, and the `selection` part says
51
51
  * how many are selected, in `selection-label`.
52
52
  *
53
+ * **Under `selection-mode` the strip is a grid**, so the selection is state a
54
+ * reader is told rather than words alone: each column of the strip is a `row`,
55
+ * a week with `rows="7"`, and each cell a `gridcell` named by its row in the
56
+ * table and carrying `aria-selected`. The strip says
57
+ * `aria-multiselectable` under `multiple` and points at the cell the keys rest
58
+ * on with `aria-activedescendant`, so a screen reader reads that cell, and the
59
+ * `live` part stays quiet. Without `selection-mode` nothing is selectable, and
60
+ * the strip is an application with the readout, as every chart's plot is.
61
+ *
62
+ * **Readings are sent as every chart sends them.** `helix-reading-change`
63
+ * says each move of the reading, by pointer, tap or key, and
64
+ * `helix-reading-press` a reading clicked, tapped, or pressed with Enter or
65
+ * Space, each with the row of the table in `index`.
66
+ *
67
+ * **Nothing to draw.** A table with no readings shows `empty-text`, "No
68
+ * readings", under the strip, and `loading` shows `loading-text` there until
69
+ * readings arrive. The `empty` and `loading` slots take markup in place of
70
+ * either, as on every chart.
71
+ *
53
72
  * **Keyboard.** The strip is one tab stop once it has cells.
54
73
  *
55
74
  * | Key | What it does |
@@ -59,7 +78,7 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
59
78
  * | Right, Left | Moves a column on or back, a week with `rows="7"`. On a right-to-left page, Left moves on. |
60
79
  * | Home, End | Moves to the first or the last cell. |
61
80
  * | Page Down, Page Up | Moves a month on or back where the rows carry dates, and four columns otherwise. |
62
- * | Enter, Space | Selects the cell, or lets it go, under `selection-mode`. |
81
+ * | Enter, Space | Presses the cell, which sends `helix-reading-press`, and selects it or lets it go under `selection-mode`. |
63
82
  * | Shift with an arrow, Home, End, Page Up or Page Down | Selects every cell from the last one pressed to where the key went, under `selection-mode="multiple"`. |
64
83
  * | Escape | Puts the cell's tooltip away. |
65
84
  *
@@ -69,23 +88,30 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
69
88
  * @aliases Contribution graph, Uptime strip, Calendar heatmap, Activity graph
70
89
  * @documentation https://fusion.dev/helix/components/heatmap
71
90
  *
91
+ * @slot empty - Content shown under the strip when the table has no readings, such as a link to add the first. Falls back to `empty-text`.
72
92
  * @slot end - Below the strip, at the trailing edge: usually the newest date.
93
+ * @slot loading - Content shown under the strip while `loading` is set and there are no readings yet. Falls back to `loading-text`.
73
94
  * @slot start - Below the strip, at the leading edge: usually the oldest date.
74
95
  * @slot summary - Below the strip, in the middle: a summary of the range shown.
75
96
  *
76
97
  * @event {Event} change - Fired once a press or a key has changed which cells are selected.
77
98
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a press or a key changes which cells are selected, with the `value` it would leave. Cancelable.
99
+ * @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.
100
+ * @event {HelixReadingPressEvent} helix-reading-press - A reading was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
78
101
  * @event {HelixResizeEvent} helix-resize - Fired when the number of cells drawn changes, with the new number.
79
102
  *
80
103
  * @csspart cell - One reading.
81
104
  * @csspart cell-active - The cell the pointer or the keys rest on. Set alongside `cell`.
82
105
  * @csspart cell-selected - A cell that is selected, under `selection-mode`. Set alongside `cell`.
83
- * @csspart empty - Shown when the table has no readings. It stays hidden until the table can be read: on the server and during hydration, there is no document to resolve `for` in.
106
+ * @csspart empty - The words under the strip 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, there being no document to resolve `for` in.
84
107
  * @csspart end - The trailing slot's box.
85
108
  * @csspart footer - The three-slot row under the strip.
86
109
  * @csspart live - The readout: a polite live region that says the cell the keys moved to. Hide it visually.
110
+ * @csspart loading - The words under the strip while `loading` is set and there are no readings: the `loading` slot, falling back to `loading-text`.
87
111
  * @csspart mark - The invisible point the tooltip is anchored to.
88
112
  * @csspart overview - The busiest reading and the total of the readings drawn, written as a sentence for screen readers, which describes the strip. Rendered once there are cells. Hide it visually.
113
+ * @csspart plot - The same element as `strip`, under the name every chart gives the drawing that takes focus.
114
+ * @csspart row - A column of cells, which a grid reads as a row, under `selection-mode`. It draws no box.
89
115
  * @csspart selection - A polite region that says how many cells are selected after a change, rendered from the first change. Hide it visually.
90
116
  * @csspart start - The leading slot's box.
91
117
  * @csspart strip - The grid of cells, which takes focus to be read by key. It is named by the host's `aria-labelledby` or `aria-label`, then by `label`, then by the caption of the table.
@@ -125,6 +151,7 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
125
151
  *
126
152
  * @cssstate active - While a cell is shown, under the pointer or the keys, with its tooltip open.
127
153
  * @cssstate empty - While the `empty` part stands in place of the cells.
154
+ * @cssstate loading - While the `loading` part stands in place of the cells.
128
155
  */
129
156
  export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
130
157
  /**
@@ -137,13 +164,6 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
137
164
  * `overview-label`.
138
165
  */
139
166
  busiestLabel: string;
140
- /**
141
- * Shown when the table has no readings in it.
142
- *
143
- * Never shown while the table cannot be read at all, which is a server render
144
- * and the hydrating one after it: see `sourceKnown`.
145
- */
146
- emptyText: string;
147
167
  /**
148
168
  * Space between cells, in CSS pixels. It is a number for the same reason
149
169
  * `min-cell` is.
@@ -187,9 +207,11 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
187
207
  */
188
208
  rows: number;
189
209
  /**
190
- * What the readout adds for a selected cell, under `selection-mode`, after
191
- * where the cell stands: "Day 1: 0 (1 of 14, selected)". Empty adds
192
- * nothing.
210
+ * What the readout added for a selected cell, under `selection-mode`.
211
+ *
212
+ * @deprecated The strip is a grid under `selection-mode`, and each cell says
213
+ * whether it is selected with `aria-selected`, in the reader's own words, so
214
+ * the readout says nothing there and this is not read.
193
215
  */
194
216
  selectedLabel: string;
195
217
  /**
@@ -334,28 +356,6 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
334
356
  * the one under the pointer.
335
357
  */
336
358
  private get points();
337
- /**
338
- * Whether this render can tell a table with no readings from one it has not
339
- * resolved yet.
340
- *
341
- * **Unknown is not empty, which is what an empty state must never say.**
342
- * `for` names an id and resolving an id wants a document, so a server has
343
- * none and a hydrating render must not look either. Without this the server
344
- * drew "No readings" over a table holding ninety days of them, a crawler read
345
- * that sentence, and the strip flipped to a full one on hydration: the fault
346
- * `MixinCollection` names for a slotted collection, arriving through the one
347
- * channel that mixin cannot see.
348
- *
349
- * The same shape as `childrenKnown`, asked of a tree that can answer about
350
- * ids rather than of one that holds children. An id that resolves to nothing
351
- * in a browser is still known, and still empty: a `for` naming no table
352
- * should say so rather than sit blank, which is why this asks
353
- * `searchableById` and not `elementById`.
354
- *
355
- * `readingsKnown` asks it for every chart, and for `data` too, which a
356
- * server does not draw.
357
- */
358
- private get sourceKnown();
359
359
  /**
360
360
  * `rows` as a usable count, which is not always what the attribute says.
361
361
  *
@@ -432,6 +432,23 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
432
432
  private markEmpty;
433
433
  protected updated(changed: PropertyValues): void;
434
434
  protected render(): TemplateResult;
435
+ /**
436
+ * The cells as a grid's rows, one for each column of the strip, so a week
437
+ * is a row with `rows="7"` and a status strip is one row.
438
+ *
439
+ * **A column, because that is the order the cells are in.** The strip fills
440
+ * down the column and then across, so a row wrapping a column keeps every
441
+ * cell where it was, and `display: contents` keeps the wrapper out of the
442
+ * strip's grid, drawing no box, which is a mechanism and not a look. A row
443
+ * across the strip would have taken cells out of order and moved them.
444
+ */
445
+ private renderRows;
446
+ /**
447
+ * One cell under `selection-mode`: a `gridcell` named by its row in the
448
+ * table, its reading and its place, and saying whether it is selected, which
449
+ * a reader hears on reaching it as the strip's active descendant.
450
+ */
451
+ private renderGridCell;
435
452
  /**
436
453
  * What the strip shows, in a sentence: the busiest reading drawn, as the
437
454
  * table writes it, and the total of them all, in `locale`. The strip is
@@ -461,13 +478,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
461
478
  * that is usually wider, so a percentage of the box lands between cells. The
462
479
  * pitch is `cell + gap` and both are measured.
463
480
  */
464
- protected renderTooltip(): TemplateResult | typeof nothing;
465
- /**
466
- * The readout's cursor: the pointer, with the selected word after where a
467
- * selected cell stands, so a reader moving through the strip hears which
468
- * cells are in the selection and not only how many.
469
- */
470
- private reading;
481
+ protected renderTooltip(grid?: boolean): TemplateResult | typeof nothing;
471
482
  /**
472
483
  * Which drawn cell the pointer is over, or -1.
473
484
  *
@@ -476,6 +487,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
476
487
  * `mouseover` it is delivered for a pen.
477
488
  */
478
489
  private readonly onPointerOver;
490
+ private readonly onPointerUp;
479
491
  private readonly onPointerLeave;
480
492
  }
481
493
  declare global {
@@ -4,16 +4,15 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, byRow, CHART_PLOT, commaList, customElement, drawTooltip, formatWords, HelixElement, HelixResizeEvent, MixinChartSource, markRegion, pressedValue, rangeAdd, requestValue, StatusController, words, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, byRow, CHART_PLOT, commaList, customElement, drawTooltip, formatWords, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, HelixResizeEvent, MixinChartSource, MixinChartStatus, markRegion, pressedValue, rangeAdd, requestValue, StatusController, spokenReading, words, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
11
11
  import { fits, monthPage, overviewOf, toneOf } from './buckets.js';
12
- let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(HelixElement) {
12
+ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartStatus(MixinChartSource(HelixElement)) {
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.busiestLabel = 'Busiest';
16
- this.emptyText = 'No readings';
17
16
  this.gap = 2;
18
17
  this.minCell = 8;
19
18
  this.overviewLabel = '{busiestLabel}: {busiest}; {totalLabel} {total}.';
@@ -69,6 +68,16 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
69
68
  const index = Number(cell?.getAttribute('data-index') ?? -1);
70
69
  this.pointer.set(Number.isInteger(index) ? index : -1);
71
70
  };
71
+ this.onPointerUp = (event) => {
72
+ if (event.pointerType !== 'touch') {
73
+ return;
74
+ }
75
+ const cell = event.target?.closest?.('[part~="cell"]');
76
+ const index = Number(cell?.getAttribute('data-index') ?? -1);
77
+ if (cell && Number.isInteger(index) && index >= 0) {
78
+ this.pointer.tapTo(index);
79
+ }
80
+ };
72
81
  this.onPointerLeave = () => {
73
82
  if (!this.pointer.keyed) {
74
83
  this.pointer.set(-1);
@@ -112,9 +121,6 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
112
121
  }
113
122
  return byRow(series, this.sourceRows.length).map((point) => point ?? { value: undefined });
114
123
  }
115
- get sourceKnown() {
116
- return this.readingsKnown;
117
- }
118
124
  get rowCount() {
119
125
  return Math.max(1, Math.floor(this.rows) || 1);
120
126
  }
@@ -160,6 +166,12 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
160
166
  }
161
167
  connectedCallback() {
162
168
  super.connectedCallback();
169
+ this.pointer.axis = 'none';
170
+ this.pointer.onReading = (heard, detail) => {
171
+ const row = detail.index < 0 ? -1 : this.points.length - this.drawn.length + detail.index;
172
+ const told = { ...detail, index: row, label: this.sourceRows[row]?.label ?? '' };
173
+ this.dispatchEvent(heard === 'press' ? new HelixReadingPressEvent(told) : new HelixReadingChangeEvent(told));
174
+ };
163
175
  this.addEventListener('keydown', this.onKeydown);
164
176
  if (typeof ResizeObserver !== 'undefined') {
165
177
  this.observer = new ResizeObserver((entries) => {
@@ -184,8 +196,6 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
184
196
  super.updated(changed);
185
197
  this.markEmpty();
186
198
  this.measure();
187
- this.toggleState('active', this.pointer.at >= 0);
188
- this.toggleState('empty', this.drawn.length === 0 && this.sourceKnown);
189
199
  const showing = this.visible;
190
200
  const written = showing > 0 ? String(showing) : null;
191
201
  if (this.getAttribute('visible') !== written) {
@@ -206,41 +216,48 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
206
216
  const palette = writtenList(this.tones);
207
217
  const cells = this.drawn;
208
218
  const readable = cells.length > 0;
219
+ const grid = readable && this.selectionMode !== undefined;
209
220
  const keys = this.selectionMode ? this.keysOf(cells) : [];
210
221
  const chosen = new Set(this.selectionMode ? commaList(this.value) : []);
222
+ const drawn = cells.map((reading, index) => {
223
+ const tone = toneOf(reading, bounds, palette);
224
+ const active = this.pointer.at === index;
225
+ const picked = chosen.has(keys[index] ?? '');
226
+ if (grid) {
227
+ return this.renderGridCell(index, active, picked, tone);
228
+ }
229
+ return tone === undefined
230
+ ? html `<span
231
+ part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
232
+ data-index=${index}
233
+ ></span>`
234
+ : html `<span
235
+ part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
236
+ data-index=${index}
237
+ data-tone=${tone}
238
+ style="--heatmap-tone: var(--heatmap-tone-${tone})"
239
+ ></span>`;
240
+ });
241
+ const at = this.pointer.at;
211
242
  return html `<div
243
+ aria-activedescendant=${grid && at >= 0 ? `cell-${at}` : nothing}
212
244
  aria-describedby=${readable ? 'helix-chart-summary' : nothing}
213
245
  aria-label=${this.named || this.stripLabel}
214
- part="strip"
246
+ aria-multiselectable=${grid && this.selectionMode === 'multiple' ? 'true' : nothing}
247
+ part="strip plot"
215
248
  id=${CHART_PLOT}
216
- role=${readable ? 'application' : 'img'}
249
+ role=${grid ? 'grid' : readable ? 'application' : 'img'}
217
250
  style="--heatmap-rows: ${this.rowCount}"
218
251
  tabindex=${readable ? '0' : nothing}
219
252
  @click=${this.onClick}
220
253
  @pointerover=${this.onPointerOver}
221
254
  @pointerleave=${this.onPointerLeave}
255
+ @pointerup=${this.onPointerUp}
222
256
  >
223
- ${cells.length === 0
224
- ? nothing
225
- : cells.map((reading, index) => {
226
- const tone = toneOf(reading, bounds, palette);
227
- const active = this.pointer.at === index;
228
- const picked = chosen.has(keys[index] ?? '');
229
- return tone === undefined
230
- ? html `<span
231
- part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
232
- data-index=${index}
233
- ></span>`
234
- : html `<span
235
- part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
236
- data-index=${index}
237
- data-tone=${tone}
238
- style="--heatmap-tone: var(--heatmap-tone-${tone})"
239
- ></span>`;
240
- })}
241
- ${this.renderTooltip()}
257
+ ${grid ? this.renderRows(drawn) : drawn}
258
+ ${this.renderTooltip(grid)}
242
259
  </div>
243
- ${cells.length === 0 && this.sourceKnown ? html `<p part="empty">${this.emptyText}</p>` : nothing}
260
+ ${cells.length === 0 ? this.renderStatus() : nothing}
244
261
  <div part="footer">
245
262
  <span part="start"><slot name="start"></slot></span>
246
263
  <span part="summary"><slot name="summary"></slot></span>
@@ -249,6 +266,28 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
249
266
  ? html `<span part="selection" role="status">${this.status.said}</span>`
250
267
  : nothing}${this.renderTable()}`;
251
268
  }
269
+ renderRows(cells) {
270
+ const size = this.rowCount === 1 ? cells.length : this.rowCount;
271
+ const rows = [];
272
+ for (let start = 0; start < cells.length; start += size) {
273
+ rows.push(html `<div part="row" role="row" style="display: contents">${cells.slice(start, start + size)}</div>`);
274
+ }
275
+ return rows;
276
+ }
277
+ renderGridCell(index, active, picked, tone) {
278
+ const row = this.sourceRows[this.points.length - this.drawn.length + index];
279
+ const name = row ? spokenReading(row) : '';
280
+ return html `<span
281
+ aria-label=${name || nothing}
282
+ aria-selected=${picked ? 'true' : 'false'}
283
+ data-index=${index}
284
+ data-tone=${tone ?? nothing}
285
+ id="cell-${index}"
286
+ part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
287
+ role="gridcell"
288
+ style=${tone === undefined ? nothing : `--heatmap-tone: var(--heatmap-tone-${tone})`}
289
+ ></span>`;
290
+ }
252
291
  renderOverview(cells) {
253
292
  const rows = this.sourceRows;
254
293
  const offset = this.points.length - cells.length;
@@ -264,10 +303,10 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
264
303
  });
265
304
  return html `<p id="helix-chart-summary" part="overview">${said}</p>`;
266
305
  }
267
- renderTooltip() {
306
+ renderTooltip(grid = false) {
268
307
  const at = this.pointer.at;
269
308
  if (at < 0) {
270
- return drawTooltip(this.sourceRows, -1, this.tag('tooltip'), undefined, undefined, this.pointer);
309
+ return drawTooltip(this.sourceRows, -1, this.tag('tooltip'), undefined, undefined, grid ? undefined : this.pointer);
271
310
  }
272
311
  const offset = this.points.length - this.drawn.length;
273
312
  const cell = this.cellPx;
@@ -278,34 +317,13 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
278
317
  placement: 'top',
279
318
  x: `${column * (cell + gap) + cell / 2}px`,
280
319
  y: `${row * (cell + gap)}px`,
281
- }, undefined, this.reading(at));
282
- }
283
- reading(at) {
284
- const pointer = this.pointer;
285
- const key = this.keysOf(this.drawn)[at] ?? '';
286
- if (!this.selectionMode || key === '' || this.selectedLabel === '') {
287
- return pointer;
288
- }
289
- if (!commaList(this.value).includes(key)) {
290
- return pointer;
291
- }
292
- return {
293
- column: pointer.column,
294
- keyed: pointer.keyed,
295
- position: pointer.position,
296
- shows: (cell) => pointer.shows(cell),
297
- state: this.selectedLabel,
298
- };
320
+ }, undefined, grid ? undefined : this.pointer);
299
321
  }
300
322
  };
301
323
  __decorate([
302
324
  words,
303
325
  property({ attribute: 'busiest-label' })
304
326
  ], HelixHeadlessHeatmap.prototype, "busiestLabel", void 0);
305
- __decorate([
306
- words,
307
- property({ attribute: 'empty-text' })
308
- ], HelixHeadlessHeatmap.prototype, "emptyText", void 0);
309
327
  __decorate([
310
328
  property({ type: Number })
311
329
  ], HelixHeadlessHeatmap.prototype, "gap", void 0);
@@ -436,6 +436,12 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
436
436
  protected renderMark(submenu: boolean): TemplateResult | typeof nothing;
437
437
  /** Whether the row says what it is from its host, for a field's `aria-activedescendant`. */
438
438
  private get onHost();
439
+ /**
440
+ * Presses the row, as `click()` presses a native button: passed to the
441
+ * control inside, which is what answers a click, so a script's `click()`
442
+ * chooses the row, or follows its link, once.
443
+ */
444
+ click(): void;
439
445
  protected render(): TemplateResult | typeof nothing;
440
446
  }
441
447
  declare global {
@@ -240,6 +240,15 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
240
240
  get onHost() {
241
241
  return this.virtual && this.onPage && !this.showingSubmenu;
242
242
  }
243
+ click() {
244
+ const control = this.partNamed('item');
245
+ if (control) {
246
+ control.click();
247
+ }
248
+ else {
249
+ super.click();
250
+ }
251
+ }
243
252
  render() {
244
253
  const submenu = this.hasSubmenu;
245
254
  const ticks = this.choice;
@@ -89,6 +89,15 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
89
89
  * Whether the notification starts as read, used until `read` is set.
90
90
  */
91
91
  defaultRead: boolean;
92
+ /**
93
+ * The dismiss control's name, read by screen readers, with `{label}` for the
94
+ * row's own words.
95
+ *
96
+ * One sentence with the words in it, so a translation puts them where its
97
+ * language does: `registerWords('de', { 'helix-notification': { dismissLabel:
98
+ * '{label} schließen' } })`.
99
+ */
100
+ dismissLabel: string;
92
101
  /**
93
102
  * Locale for the timestamp.
94
103
  *
@@ -129,13 +138,6 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
129
138
  * the first render on the client is the server's.
130
139
  */
131
140
  stop: boolean;
132
- /**
133
- * What a screen reader hears before an unread notification's text.
134
- *
135
- * The unread mark is drawn, so a reader who cannot see it hears this word in
136
- * its place. Translate it with the rest of your interface.
137
- */
138
- unreadLabel: string;
139
141
  /**
140
142
  * @internal Read by the list to identify its children across the light DOM,
141
143
  * which is cheaper than `instanceof` and is why this is public rather than
@@ -231,11 +233,37 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
231
233
  */
232
234
  private renderTime;
233
235
  protected render(): TemplateResult;
236
+ /**
237
+ * What a screen reader hears before an unread notification's text.
238
+ *
239
+ * The unread mark is drawn, so a reader who cannot see it hears this word in
240
+ * its place. Translate it with the rest of your interface.
241
+ */
242
+ unreadLabel: string;
243
+ /**
244
+ * The dismiss control's name: `dismissLabel` with the row's words.
245
+ *
246
+ * **Named for its row**, because a feed of rows each with a control named
247
+ * Dismiss is a list of buttons that says nothing about which notice each one
248
+ * ends. The words are the default slot's, the ones `helix-dismiss` reports,
249
+ * so a row's actions are not read into it. A page's own `dismiss-label` with
250
+ * no `{label}` is followed by them, unless it holds them already, as `Tag`
251
+ * names its remove button. Read from the children, so the verb alone on a
252
+ * server, until the update after.
253
+ */
254
+ private get dismissName();
255
+ protected firstUpdated(changed: PropertyValues): void;
234
256
  private readonly handleActions;
235
257
  private readonly handleDismiss;
236
258
  /**
237
- * Where focus goes when this row leaves: the next row's first control, the
238
- * one before, then the first control left in the feed.
259
+ * Where focus goes when this row leaves: the next row, the one before, then
260
+ * the first control left in the feed.
261
+ *
262
+ * **The row itself, and not a control in it**, because the row is what a
263
+ * reader moves through the feed by, and every row takes focus. Sent to the
264
+ * next dismiss control, focus passed over each row with no controls between,
265
+ * and to Load more where no row after had one. Focused, the row becomes the
266
+ * feed's stop, as one reached with the arrows does.
239
267
  */
240
268
  private neighbor;
241
269
  }
@@ -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, deepActiveElement, deepContains, focusableElements, HelixElement, HelixReadEvent, MixinDismissible, requestChange, slotFilled, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, deepActiveElement, deepContains, focusableElements, formatWords, HelixElement, HelixReadEvent, MixinDismissible, requestChange, slotFilled, slottedWords, words, } from '@fusion.dev/helix-core';
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -16,13 +16,14 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
16
16
  super(...arguments);
17
17
  this.at = '';
18
18
  this.defaultRead = false;
19
+ this.dismissLabel = 'Dismiss {label}';
19
20
  this.locale = '';
20
21
  this.position = 0;
21
22
  this.setSize = 0;
22
23
  this.stop = true;
23
- this.unreadLabel = 'Unread';
24
24
  this.isNotification = true;
25
25
  this.readState = undefined;
26
+ this.unreadLabel = 'Unread';
26
27
  this.handleActions = () => {
27
28
  this.markActions();
28
29
  };
@@ -82,7 +83,7 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
82
83
  return html `<button
83
84
  part="dismiss"
84
85
  type="button"
85
- aria-label=${this.dismissLabel}
86
+ aria-label=${this.dismissName}
86
87
  @click=${this.handleDismiss}
87
88
  >
88
89
  <slot name="dismiss">
@@ -132,6 +133,18 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
132
133
  </div>
133
134
  </div>`;
134
135
  }
136
+ get dismissName() {
137
+ const { dismissLabel } = this;
138
+ const label = this.childrenKnown ? slottedWords(this, ['']) : '';
139
+ if (dismissLabel.includes('{label}')) {
140
+ return formatWords(this, 'dismissLabel', { label }).trim();
141
+ }
142
+ return label === '' || dismissLabel.includes(label) ? dismissLabel : `${dismissLabel} ${label}`;
143
+ }
144
+ firstUpdated(changed) {
145
+ super.firstUpdated(changed);
146
+ this.renderRoot.addEventListener('slotchange', () => this.requestUpdate());
147
+ }
135
148
  neighbor() {
136
149
  let feed = this.parentElement;
137
150
  while (feed && !('markEveryoneRead' in feed)) {
@@ -143,7 +156,7 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
143
156
  const at = rows.indexOf(this);
144
157
  const order = [...rows.slice(at + 1), ...rows.slice(0, Math.max(at, 0)).reverse()];
145
158
  for (const row of order) {
146
- const target = row.shadowRoot?.querySelector('[part="dismiss"]') ?? focusableElements(row)[0];
159
+ const target = row.shadowRoot?.querySelector('[part="item"]');
147
160
  if (target) {
148
161
  return target;
149
162
  }
@@ -162,6 +175,10 @@ __decorate([
162
175
  __decorate([
163
176
  property({ attribute: 'default-read', type: Boolean })
164
177
  ], HelixHeadlessNotification.prototype, "defaultRead", void 0);
178
+ __decorate([
179
+ words,
180
+ property({ attribute: 'dismiss-label' })
181
+ ], HelixHeadlessNotification.prototype, "dismissLabel", void 0);
165
182
  __decorate([
166
183
  property()
167
184
  ], HelixHeadlessNotification.prototype, "locale", void 0);
@@ -87,6 +87,12 @@ export declare class HelixHeadlessSpeedDialItem extends HelixElement {
87
87
  * keyboard controller uses for the arrow keys.
88
88
  */
89
89
  focusItem(): void;
90
+ /**
91
+ * Presses the action, as `click()` presses a native button: passed to the
92
+ * control inside, which is what answers a click, so a script's `click()`
93
+ * chooses the action and follows its link, once.
94
+ */
95
+ click(): void;
90
96
  /** Chooses the action, the same as pressing it. */
91
97
  select(): void;
92
98
  /**
@@ -43,6 +43,15 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
43
43
  focusItem() {
44
44
  this.focus();
45
45
  }
46
+ click() {
47
+ const control = this.partNamed('item');
48
+ if (control) {
49
+ control.click();
50
+ }
51
+ else {
52
+ super.click();
53
+ }
54
+ }
46
55
  select() {
47
56
  this.choose();
48
57
  }
@@ -112,6 +112,12 @@ export declare class HelixHeadlessTab extends HelixHeadlessTab_base {
112
112
  * which changes nothing and so re-renders nothing.
113
113
  */
114
114
  protected firstUpdated(changed: PropertyValues): void;
115
+ /**
116
+ * Presses the tab, as `click()` presses a native button: passed to the
117
+ * control inside, which is what answers a click, so a script's `click()`
118
+ * selects the tab, or follows its link, once.
119
+ */
120
+ click(): void;
115
121
  protected render(): TemplateResult;
116
122
  }
117
123
  declare global {
@@ -67,6 +67,15 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSele
67
67
  super.firstUpdated(changed);
68
68
  this.finishHydration();
69
69
  }
70
+ click() {
71
+ const control = this.partNamed('tab');
72
+ if (control) {
73
+ control.click();
74
+ }
75
+ else {
76
+ super.click();
77
+ }
78
+ }
70
79
  render() {
71
80
  const tabindex = this.selected || !this.childrenKnown ? '0' : '-1';
72
81
  const shortcut = this.dismissible && !this.disabled ? 'Delete' : nothing;