@fusion.dev/helix-headless 0.178.0 → 0.180.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 (54) hide show
  1. package/custom-elements.json +543 -145
  2. package/dist/button/root/button-root.d.ts +12 -0
  3. package/dist/button/root/button-root.js +9 -0
  4. package/dist/chat/composer/chat-composer.d.ts +2 -1
  5. package/dist/chat/composer/chat-composer.js +1 -1
  6. package/dist/combobox/root/combobox-root.d.ts +12 -0
  7. package/dist/combobox/root/combobox-root.js +3 -2
  8. package/dist/command/root/command-root.d.ts +12 -0
  9. package/dist/command/root/command-root.js +3 -2
  10. package/dist/data-table/root/data-table-root.d.ts +13 -1
  11. package/dist/data-table/root/data-table-root.js +18 -15
  12. package/dist/date-input/root/date-input-root.d.ts +7 -0
  13. package/dist/date-input/root/date-input-root.js +12 -0
  14. package/dist/date-picker/root/date-picker-root.d.ts +7 -1
  15. package/dist/date-picker/root/date-picker-root.js +12 -0
  16. package/dist/drilldown/item/drilldown-item.d.ts +6 -0
  17. package/dist/drilldown/item/drilldown-item.js +9 -0
  18. package/dist/drilldown/root/drilldown-root.js +9 -0
  19. package/dist/element-events.generated.js +7 -1
  20. package/dist/filter-input/root/filter-input-root.d.ts +13 -0
  21. package/dist/filter-input/root/filter-input-root.js +2 -1
  22. package/dist/heatmap/root/heatmap-root.d.ts +54 -42
  23. package/dist/heatmap/root/heatmap-root.js +72 -54
  24. package/dist/listbox/root/listbox-root.d.ts +16 -2
  25. package/dist/listbox/root/listbox-root.js +8 -4
  26. package/dist/menu/item/menu-item.d.ts +6 -0
  27. package/dist/menu/item/menu-item.js +9 -0
  28. package/dist/meter/root/meter-root.js +1 -2
  29. package/dist/notifications/notification/notification.d.ts +37 -9
  30. package/dist/notifications/notification/notification.js +35 -5
  31. package/dist/option/among.d.ts +23 -0
  32. package/dist/option/among.js +2 -0
  33. package/dist/option/root/option-root.d.ts +1 -17
  34. package/dist/option/root/option-root.js +1 -4
  35. package/dist/scatter-chart/root/scatter-chart-root.d.ts +5 -4
  36. package/dist/scatter-chart/root/scatter-chart-root.js +1 -0
  37. package/dist/select/root/select-root.d.ts +13 -2
  38. package/dist/select/root/select-root.js +3 -3
  39. package/dist/speed-dial/item/speed-dial-item.d.ts +6 -0
  40. package/dist/speed-dial/item/speed-dial-item.js +9 -0
  41. package/dist/table/column/table-column.d.ts +1 -10
  42. package/dist/table/column/table-column.js +1 -2
  43. package/dist/table/edge.d.ts +10 -0
  44. package/dist/table/edge.js +2 -0
  45. package/dist/tabs/root/tabs-root.d.ts +1 -0
  46. package/dist/tabs/root/tabs-root.js +13 -0
  47. package/dist/tabs/tab/tab.d.ts +6 -0
  48. package/dist/tabs/tab/tab.js +9 -0
  49. package/dist/tooltip/root/tooltip-root.js +1 -0
  50. package/lint.json +1 -1
  51. package/package.json +5 -5
  52. package/types/jsx.d.ts +472 -130
  53. package/types/vue.d.ts +71 -50
  54. package/types/words.d.ts +27 -6
@@ -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);
@@ -1,6 +1,6 @@
1
1
  import { AriaForwardController, type DataOptionItem, DisabledCascadeController, HelixFormElement, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
- import { HelixHeadlessOption } from '../../option/root/option-root.js';
3
+ import type { HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
5
5
  /**
6
6
  * @summary A list of options, from which you choose one or several.
@@ -76,6 +76,19 @@ declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusio
76
76
  * filter narrows the list, under `layout="grid"`, or while the list is
77
77
  * disabled.
78
78
  *
79
+ * **The options are an import of their own.** An option you write is the Option
80
+ * family's, which the list does not register, so a page that writes one imports
81
+ * it beside the list:
82
+ *
83
+ * ```js
84
+ * import '@fusion.dev/helix/listbox'
85
+ * import '@fusion.dev/helix/option'
86
+ * ```
87
+ *
88
+ * In development, a list holding an option nobody registered says so once the
89
+ * page has loaded, with the line to add. The options `items` draws are this
90
+ * Option too, and need the same import.
91
+ *
79
92
  * @since 0.1.0
80
93
  * @status stable
81
94
  * @category forms
@@ -218,7 +231,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
218
231
  *
219
232
  * **The list needs a height to scroll in**: the styled tier's
220
233
  * `--listbox-max-block-size`, or a `max-block-size` on `::part(items)` in the
221
- * headless tier. Import the Option family as well, whose elements are drawn.
234
+ * headless tier. The options drawn are this tier's Option, which the page
235
+ * imports, as it imports the options it writes.
222
236
  * A server sends the list empty, and the options are drawn once it hydrates.
223
237
  */
224
238
  items: readonly DataOptionItem[] | undefined;
@@ -4,12 +4,12 @@ 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, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, filterKeeps, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, auditChildren, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, filterKeeps, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- import { HelixHeadlessOption } from '../../option/root/option-root.js';
12
+ import { isOption } from '../../option/among.js';
13
13
  const FIELD_MOVES = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
14
14
  const MOVES = new Set([...FIELD_MOVES, 'End', 'Home']);
15
15
  const PAGE = 10;
@@ -306,7 +306,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
306
306
  return this.multiple ? 'select-multiple' : 'select-one';
307
307
  }
308
308
  get options() {
309
- return this.among((node) => node instanceof HelixHeadlessOption, this.children);
309
+ return this.among(isOption, this.children);
310
310
  }
311
311
  among(is, nodes) {
312
312
  const found = [];
@@ -421,7 +421,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
421
421
  option.selected = chosen.has(option.value);
422
422
  }
423
423
  for (const group of this.groups) {
424
- const inside = this.among((node) => node instanceof HelixHeadlessOption, group.children);
424
+ const inside = this.among(isOption, group.children);
425
425
  group.hidden = inside.length > 0 && inside.every((option) => option.hidden);
426
426
  }
427
427
  for (const rule of this.among((node) => node.localName.endsWith('-option-separator'), this.children)) {
@@ -479,6 +479,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
479
479
  this.addEventListener('helix-select', this.handleSelection);
480
480
  this.addEventListener('keydown', this.handleKeydown);
481
481
  this.addEventListener('click', this.handleClick, true);
482
+ auditChildren(this, 'option', 'option', 'the list offers no choices');
482
483
  }
483
484
  disconnectedCallback() {
484
485
  super.disconnectedCallback();
@@ -500,6 +501,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
500
501
  if (this.childrenKnown) {
501
502
  this.seedDeclared();
502
503
  }
504
+ if (changed.has('items') && this.items) {
505
+ auditChildren(this, 'option', 'option', 'the items draw no choices', true);
506
+ }
503
507
  if (this.childrenKnown) {
504
508
  this.place();
505
509
  this.tell(changed);
@@ -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;
@@ -83,8 +83,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
83
83
  this.internals.ariaValueMin = String(min);
84
84
  this.internals.ariaValueMax = String(max);
85
85
  this.internals.ariaValueNow = String(this.reading);
86
- this.internals.ariaValueText =
87
- this.valueText || this.formatOptions !== undefined || this.beyond ? this.text : null;
86
+ this.internals.ariaValueText = this.text;
88
87
  const band = this.computedBand;
89
88
  const beyond = this.beyond;
90
89
  this.toggleState('fair', band === 'fair');
@@ -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
  };
@@ -30,8 +31,14 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
30
31
  event.stopPropagation();
31
32
  if (this.dismiss()) {
32
33
  const next = deepContains(this, deepActiveElement()) ? this.neighbor() : null;
34
+ const holder = this.parentElement;
33
35
  this.remove();
34
36
  next?.focus();
37
+ if (next && next.getRootNode().activeElement !== next) {
38
+ void Promise.resolve()
39
+ .then(() => holder?.updateComplete)
40
+ .then(() => next.focus());
41
+ }
35
42
  }
36
43
  };
37
44
  }
@@ -82,7 +89,7 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
82
89
  return html `<button
83
90
  part="dismiss"
84
91
  type="button"
85
- aria-label=${this.dismissLabel}
92
+ aria-label=${this.dismissName}
86
93
  @click=${this.handleDismiss}
87
94
  >
88
95
  <slot name="dismiss">
@@ -132,6 +139,18 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
132
139
  </div>
133
140
  </div>`;
134
141
  }
142
+ get dismissName() {
143
+ const { dismissLabel } = this;
144
+ const label = this.childrenKnown ? slottedWords(this, ['']) : '';
145
+ if (dismissLabel.includes('{label}')) {
146
+ return formatWords(this, 'dismissLabel', { label }).trim();
147
+ }
148
+ return label === '' || dismissLabel.includes(label) ? dismissLabel : `${dismissLabel} ${label}`;
149
+ }
150
+ firstUpdated(changed) {
151
+ super.firstUpdated(changed);
152
+ this.renderRoot.addEventListener('slotchange', () => this.requestUpdate());
153
+ }
135
154
  neighbor() {
136
155
  let feed = this.parentElement;
137
156
  while (feed && !('markEveryoneRead' in feed)) {
@@ -141,9 +160,16 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
141
160
  ? feed.items
142
161
  : [...(this.parentElement?.children ?? [])].filter((one) => 'isNotification' in one);
143
162
  const at = rows.indexOf(this);
144
- const order = [...rows.slice(at + 1), ...rows.slice(0, Math.max(at, 0)).reverse()];
163
+ const group = this.parentElement;
164
+ const promoted = group && 'isNotificationGroup' in group && this.assignedSlot !== null
165
+ ? [...group.children].find((one) => one !== this && 'isNotification' in one)
166
+ : undefined;
167
+ const drawn = (row) => !('isNotificationGroup' in (row.parentElement ?? {})) ||
168
+ row.assignedSlot !== null ||
169
+ row === promoted;
170
+ const order = [...rows.slice(at + 1), ...rows.slice(0, Math.max(at, 0)).reverse()].filter(drawn);
145
171
  for (const row of order) {
146
- const target = row.shadowRoot?.querySelector('[part="dismiss"]') ?? focusableElements(row)[0];
172
+ const target = row.shadowRoot?.querySelector('[part="item"]');
147
173
  if (target) {
148
174
  return target;
149
175
  }
@@ -162,6 +188,10 @@ __decorate([
162
188
  __decorate([
163
189
  property({ attribute: 'default-read', type: Boolean })
164
190
  ], HelixHeadlessNotification.prototype, "defaultRead", void 0);
191
+ __decorate([
192
+ words,
193
+ property({ attribute: 'dismiss-label' })
194
+ ], HelixHeadlessNotification.prototype, "dismissLabel", void 0);
165
195
  __decorate([
166
196
  property()
167
197
  ], HelixHeadlessNotification.prototype, "locale", void 0);
@@ -0,0 +1,23 @@
1
+ import type { HelixHeadlessOption } from './root/option-root.js';
2
+ /**
3
+ * Whether an element is an option, by its shape.
4
+ *
5
+ * **Not by `instanceof`.** A tag is defined once, by whichever copy of this
6
+ * class registered it first, and a second copy in the same document tests
7
+ * against a class no element on the page has: every option failed it, and a
8
+ * select said "Choose" over its value. Two copies are a consumer with a
9
+ * duplicated dependency, or the browser specs, where each family's bundle
10
+ * carries its own and they share one page. An option nobody registered has no
11
+ * such shape, and is no option until its entry is imported.
12
+ */
13
+ export declare const isOption: (element: Element) => element is HelixHeadlessOption;
14
+ /**
15
+ * The options among some elements, reaching one level into a group.
16
+ *
17
+ * What a control holding options reads its words from, off its own children
18
+ * rather than off the listbox they are slotted into: the slot exists only once a
19
+ * render has made it, so the listbox has none on a first render, and on a page a
20
+ * server rendered the `slotchange` that would correct it fires before anything
21
+ * listens.
22
+ */
23
+ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadlessOption[];
@@ -0,0 +1,2 @@
1
+ export const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
2
+ export const optionsAmong = (elements) => elements.flatMap((element) => isOption(element) ? [element] : [...element.children].filter(isOption));
@@ -1,22 +1,6 @@
1
1
  import { HelixElement, type LinkModifiers } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- /**
4
- * The options among some elements, reaching one level into a group.
5
- *
6
- * What a control holding options reads its words from, off its own children
7
- * rather than off the listbox they are slotted into: the slot exists only once a
8
- * render has made it, so the listbox has none on a first render, and on a page a
9
- * server rendered the `slotchange` that would correct it fires before anything
10
- * listens.
11
- *
12
- * **An option by its shape, not by `instanceof`.** A tag is defined once, by
13
- * whichever copy of this class registered it first, and a second copy in the
14
- * same document tests against a class no element on the page has: every option
15
- * failed it, and a select said "Choose" over its value. Two copies are a
16
- * consumer with a duplicated dependency, or the browser specs, where each
17
- * family's bundle carries its own and they share one page.
18
- */
19
- export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadlessOption[];
3
+ export { isOption, optionsAmong } from '../among.js';
20
4
  /**
21
5
  * @summary One choice in a listbox.
22
6
  *
@@ -7,10 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, followLink, HelixElement, HelixSelectEvent, hostAria, slotFilled, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- export const optionsAmong = (elements) => {
11
- const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
12
- return elements.flatMap((element) => isOption(element) ? [element] : [...element.children].filter(isOption));
13
- };
10
+ export { isOption, optionsAmong } from '../among.js';
14
11
  let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
15
12
  constructor() {
16
13
  super(...arguments);
@@ -62,9 +62,10 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
62
62
  *
63
63
  * The plot runs left to right in every text direction, as a number line does.
64
64
  * The plot, its axes, and the axis names sit in a `plane` part with
65
- * `dir="ltr"`. The axis names carry `dir="auto"`, so a name in Arabic or Hebrew
66
- * reads in its own direction. The summary and the tooltip bubble follow the
67
- * page's direction.
65
+ * `dir="ltr"`, and the arrow keys follow the drawing: Right moves to the next
66
+ * point on a right-to-left page too. The axis names carry `dir="auto"`, so a
67
+ * name in Arabic or Hebrew reads in its own direction. The summary and the
68
+ * tooltip bubble follow the page's direction.
68
69
  *
69
70
  * **A tap shows the reading under it**, and keeps it until a tap lands
70
71
  * elsewhere. `helix-reading-change` says each move of the reading, by pointer,
@@ -86,7 +87,7 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
86
87
  * | Key | What it does |
87
88
  * |---|---|
88
89
  * | Tab | Moves focus to the plot and shows the first reading. |
89
- * | Right, Left | Moves to the next or the previous point, left to right across the plot. On a right-to-left page, Left moves to the next. |
90
+ * | Right, Left | Moves to the next or the previous point, left to right across the plot, on a right-to-left page too. |
90
91
  * | Down, Up | Moves among the series of the point. With one series, moves to the next or the previous point. |
91
92
  * | Home, End | Moves to the first or the last point. |
92
93
  * | Page Down, Page Up | Moves a tenth of the points on or back. |
@@ -76,6 +76,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
76
76
  connectedCallback() {
77
77
  super.connectedCallback();
78
78
  this.pointer.axis = 'none';
79
+ this.pointer.drawnLtr = true;
79
80
  this.pointer.onReading = (heard, detail) => this.dispatchEvent(heard === 'press'
80
81
  ? new HelixReadingPressEvent(detail)
81
82
  : new HelixReadingChangeEvent(detail));
@@ -1,8 +1,7 @@
1
1
  import { AriaForwardController, type DataOptionItem, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import { type HelixHeadlessOption } from '../../option/root/option-root.js';
3
+ import type { HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  import '../../listbox/index.js';
5
- import '../../option/index.js';
6
5
  declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
7
6
  /**
8
7
  * @summary A control that opens a list of options to choose from.
@@ -60,6 +59,18 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
60
59
  * Set `autocomplete` to let a browser's autofill choose, as a native `<select>`
61
60
  * with the same attribute would.
62
61
  *
62
+ * **The options are an import of their own.** An option is the Option family's,
63
+ * which the select does not register, so a page that writes one imports it
64
+ * beside the select:
65
+ *
66
+ * ```js
67
+ * import '@fusion.dev/helix/select'
68
+ * import '@fusion.dev/helix/option'
69
+ * ```
70
+ *
71
+ * In development, a select holding an option nobody registered says so once the
72
+ * page has loaded, with the line to add.
73
+ *
63
74
  * @since 0.1.0
64
75
  * @status stable
65
76
  * @category forms