@fusion.dev/helix-headless 0.179.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 (40) hide show
  1. package/custom-elements.json +160 -50
  2. package/dist/chat/composer/chat-composer.d.ts +2 -1
  3. package/dist/chat/composer/chat-composer.js +1 -1
  4. package/dist/combobox/root/combobox-root.d.ts +12 -0
  5. package/dist/combobox/root/combobox-root.js +3 -2
  6. package/dist/command/root/command-root.d.ts +12 -0
  7. package/dist/command/root/command-root.js +3 -2
  8. package/dist/data-table/root/data-table-root.d.ts +13 -1
  9. package/dist/data-table/root/data-table-root.js +18 -15
  10. package/dist/date-input/root/date-input-root.d.ts +7 -0
  11. package/dist/date-input/root/date-input-root.js +12 -0
  12. package/dist/date-picker/root/date-picker-root.d.ts +7 -1
  13. package/dist/date-picker/root/date-picker-root.js +12 -0
  14. package/dist/drilldown/root/drilldown-root.js +9 -0
  15. package/dist/filter-input/root/filter-input-root.d.ts +13 -0
  16. package/dist/filter-input/root/filter-input-root.js +2 -1
  17. package/dist/listbox/root/listbox-root.d.ts +16 -2
  18. package/dist/listbox/root/listbox-root.js +8 -4
  19. package/dist/meter/root/meter-root.js +1 -2
  20. package/dist/notifications/notification/notification.js +14 -1
  21. package/dist/option/among.d.ts +23 -0
  22. package/dist/option/among.js +2 -0
  23. package/dist/option/root/option-root.d.ts +1 -17
  24. package/dist/option/root/option-root.js +1 -4
  25. package/dist/scatter-chart/root/scatter-chart-root.d.ts +5 -4
  26. package/dist/scatter-chart/root/scatter-chart-root.js +1 -0
  27. package/dist/select/root/select-root.d.ts +13 -2
  28. package/dist/select/root/select-root.js +3 -3
  29. package/dist/table/column/table-column.d.ts +1 -10
  30. package/dist/table/column/table-column.js +1 -2
  31. package/dist/table/edge.d.ts +10 -0
  32. package/dist/table/edge.js +2 -0
  33. package/dist/tabs/root/tabs-root.d.ts +1 -0
  34. package/dist/tabs/root/tabs-root.js +13 -0
  35. package/dist/tooltip/root/tooltip-root.js +1 -0
  36. package/lint.json +1 -1
  37. package/package.json +5 -5
  38. package/types/jsx.d.ts +210 -16
  39. package/types/vue.d.ts +25 -11
  40. package/types/words.d.ts +14 -0
@@ -4,11 +4,11 @@ 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, drawStatus, HelixElement, HelixSelectEvent, MixinControllerOpen, matchesChord, StatusController, words, } from '@fusion.dev/helix-core';
7
+ import { auditChildren, customElement, drawStatus, HelixElement, HelixSelectEvent, MixinControllerOpen, matchesChord, StatusController, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
- import { optionsAmong } from '../../option/root/option-root.js';
11
+ import { optionsAmong } from '../../option/among.js';
12
12
  import '../../dialog/index.js';
13
13
  import '../../listbox/index.js';
14
14
  const says = (element, state) => {
@@ -145,6 +145,7 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
145
145
  }
146
146
  connectedCallback() {
147
147
  super.connectedCallback();
148
+ auditChildren(this, 'option', 'option', 'the palette lists no commands');
148
149
  this.addEventListener('helix-select', this.swallowOptionSelect);
149
150
  this.addEventListener('keydown', this.handleAlternate, { capture: true });
150
151
  this.addEventListener('keydown', this.handleBack);
@@ -1,6 +1,6 @@
1
1
  import { type DataRow, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import { type HelixHeadlessTableColumn } from '../../table/column/table-column.js';
3
+ import type { HelixHeadlessTableColumn } from '../../table/column/table-column.js';
4
4
  import '../../checkbox/index.js';
5
5
  import '../../pagination/index.js';
6
6
  declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & typeof HelixElement;
@@ -131,6 +131,18 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
131
131
  * Written on `DataTable`, `columns` has no effect, so watch for it when you
132
132
  * move a table from `Table` to `DataTable`.
133
133
  *
134
+ * **The columns are an import of their own.** A column is the Table family's
135
+ * `TableColumn`, which the data table does not register, so a page that writes
136
+ * one imports the Table entry beside the data table:
137
+ *
138
+ * ```js
139
+ * import '@fusion.dev/helix/data-table'
140
+ * import '@fusion.dev/helix/table'
141
+ * ```
142
+ *
143
+ * In development, a data table holding a column nobody registered says so once
144
+ * the page has loaded, with the line to add.
145
+ *
134
146
  * @since 0.1.0
135
147
  * @status stable
136
148
  * @category data
@@ -5,14 +5,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessDataTable_1;
8
- import { anchorAt, arranged, auditServed, commaList, countLabel, customElement, cycle, deepActiveElement, deepContains, everyState, format, formatWords, gridMove, HelixActivateEvent, HelixElement, HelixExpandEvent, HelixFilterEvent, HelixFilteredEvent, HelixLoadMoreEvent, HelixNavigateEvent, HelixReorderEvent, HelixReorderedEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, handedLocale, hostAria, inlineStep, inRowOrder, MixinCollection, markRegion, onAndroid, ordered, pageRows, parse, placeKey, ReorderController, rangeAdd, reportRuntime, requestChange, requestReorder, scrollingAncestor, scrollRoom, selectsAll, Typeahead, toggleEvery, treeArrow, typesCharacter, VirtualizeController, valueAt, window_, words, wordsLocale, writtenList, } from '@fusion.dev/helix-core';
8
+ import { anchorAt, arranged, auditChildren, auditServed, commaList, countLabel, customElement, cycle, deepActiveElement, deepContains, everyState, format, formatWords, gridMove, HelixActivateEvent, HelixElement, HelixExpandEvent, HelixFilterEvent, HelixFilteredEvent, HelixLoadMoreEvent, HelixNavigateEvent, HelixReorderEvent, HelixReorderedEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, handedLocale, hostAria, inlineStep, inRowOrder, MixinCollection, markRegion, onAndroid, ordered, pageRows, parse, placeKey, ReorderController, rangeAdd, reportRuntime, requestChange, requestReorder, scrollingAncestor, scrollRoom, selectsAll, Typeahead, toggleEvery, treeArrow, typesCharacter, VirtualizeController, valueAt, whenHydrated, window_, words, wordsLocale, writtenList, } from '@fusion.dev/helix-core';
9
9
  import { caretDown, caretRight, dotsSixVertical } from '@fusion.dev/helix-icons';
10
10
  import { nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
12
12
  import { repeat } from 'lit/directives/repeat.js';
13
13
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
14
14
  import { html } from 'lit/static-html.js';
15
- import { edgeOf } from '../../table/column/table-column.js';
15
+ import { edgeOf } from '../../table/edge.js';
16
16
  import '../../checkbox/index.js';
17
17
  import '../../pagination/index.js';
18
18
  import { toggledOpen } from '../keys.js';
@@ -1021,19 +1021,21 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1021
1021
  const shown = this.shownColumns;
1022
1022
  const android = onAndroid();
1023
1023
  for (const column of this.tableColumns) {
1024
- column.movable = this.reorderable && column.field !== '';
1025
- column.gridCell = true;
1026
- column.tabStop = shown.indexOf(column) + offset === stop;
1027
- column.sorted =
1028
- current && column.canSort && column.field === current.field
1029
- ? current.direction === 'asc'
1030
- ? 'ascending'
1031
- : 'descending'
1032
- : undefined;
1033
- column.sortWords =
1034
- android && column.sorted
1035
- ? this.sortSaid(column.textContent?.trim() || column.field, column.sorted)
1036
- : '';
1024
+ whenHydrated(column, () => {
1025
+ column.movable = this.reorderable && column.field !== '';
1026
+ column.gridCell = true;
1027
+ column.tabStop = shown.indexOf(column) + offset === stop;
1028
+ column.sorted =
1029
+ current && column.canSort && column.field === current.field
1030
+ ? current.direction === 'asc'
1031
+ ? 'ascending'
1032
+ : 'descending'
1033
+ : undefined;
1034
+ column.sortWords =
1035
+ android && column.sorted
1036
+ ? this.sortSaid(column.textContent?.trim() || column.field, column.sorted)
1037
+ : '';
1038
+ });
1037
1039
  }
1038
1040
  }
1039
1041
  renderCell(column, row, picked, place, stop, header, id) {
@@ -1276,6 +1278,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1276
1278
  }
1277
1279
  connectedCallback() {
1278
1280
  super.connectedCallback();
1281
+ auditChildren(this, 'table-column', 'table', 'the table draws no columns');
1279
1282
  this.addEventListener('click', this.handleHeadClick);
1280
1283
  this.addEventListener('click', this.handleRowClick);
1281
1284
  this.addEventListener('pointerdown', this.notePress, true);
@@ -33,6 +33,8 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
33
33
  * digits or the arrow keys. Alt+Down opens the calendar from a segment. A
34
34
  * `range` is two rows with a dash between them, the start and the end, as
35
35
  * React Aria's DateRangePicker draws them, and `multiple` keeps the text.
36
+ * A range whose end is before its start reports `rangeUnderflow`, as Date
37
+ * Picker does, with `range-order-message`.
36
38
  *
37
39
  * Text that is not a date gives the form an empty value, as a native date
38
40
  * field does, and `value` keeps the text so it can be corrected.
@@ -359,6 +361,11 @@ export declare class HelixHeadlessDateInput extends HelixHeadlessDateInput_base
359
361
  * for the page's language, then the English.
360
362
  */
361
363
  rangeLengthMessage: string;
364
+ /**
365
+ * The message shown when a range ends before it starts. `{start}` is
366
+ * replaced with the start, written the way the field writes dates.
367
+ */
368
+ rangeOrderMessage: string;
362
369
  /**
363
370
  * The message shown when a date is after `max`. `{max}` is replaced with
364
371
  * `max`, written the way the field writes dates.
@@ -47,6 +47,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
47
47
  this.previousYearLabel = 'Previous year';
48
48
  this.previousYearsLabel = 'Earlier years';
49
49
  this.rangeLengthMessage = 'A range is two dates.';
50
+ this.rangeOrderMessage = 'Choose an end on or after {start}.';
50
51
  this.rangeOverflowMessage = 'Choose a date on or before {max}.';
51
52
  this.rangeUnderflowMessage = 'Choose a date on or after {min}.';
52
53
  this.removeLabel = 'Remove {name}';
@@ -263,6 +264,13 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
263
264
  if (this.mode === 'range' && held.length > 2) {
264
265
  return this.rangeLengthMessage;
265
266
  }
267
+ const [start, end] = held;
268
+ if (this.mode === 'range' && start !== undefined && end !== undefined && end < start) {
269
+ return [
270
+ 'rangeUnderflow',
271
+ formatWords(this, 'rangeOrderMessage', { start: this.written(start) }),
272
+ ];
273
+ }
266
274
  return '';
267
275
  }
268
276
  renderPicker() {
@@ -547,6 +555,10 @@ __decorate([
547
555
  words,
548
556
  property({ attribute: 'range-length-message' })
549
557
  ], HelixHeadlessDateInput.prototype, "rangeLengthMessage", void 0);
558
+ __decorate([
559
+ words,
560
+ property({ attribute: 'range-order-message' })
561
+ ], HelixHeadlessDateInput.prototype, "rangeOrderMessage", void 0);
550
562
  __decorate([
551
563
  words,
552
564
  property({ attribute: 'range-overflow-message' })
@@ -48,7 +48,8 @@ declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fu
48
48
  * when Previous or Next is pressed. Neither moves past `min` or `max`, and the
49
49
  * keys stop at them too. A value outside the bounds, on a barred day, or off
50
50
  * `step` reports `rangeUnderflow`, `rangeOverflow`, `badInput` or
51
- * `stepMismatch`, so a form does not submit it.
51
+ * `stepMismatch`, so a form does not submit it. A range set from a script
52
+ * whose end is before its start reports `rangeUnderflow` as well.
52
53
  *
53
54
  * @since 0.1.0
54
55
  * @status stable
@@ -278,6 +279,11 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
278
279
  * Accessible name for the control that moves back a page, in the years.
279
280
  */
280
281
  previousYearsLabel: string;
282
+ /**
283
+ * The validation message for a range that ends before it starts. `{start}`
284
+ * is replaced with the start, written as a date in `locale`.
285
+ */
286
+ rangeOrderMessage: string;
281
287
  /**
282
288
  * The validation message for a date after `max`. `{max}` is replaced with
283
289
  * `max`, written as a date in `locale`.
@@ -44,6 +44,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
44
44
  this.previousLabel = 'Previous month';
45
45
  this.previousYearLabel = 'Previous year';
46
46
  this.previousYearsLabel = 'Earlier years';
47
+ this.rangeOrderMessage = 'Choose an end on or after {start}.';
47
48
  this.rangeOverflowMessage = 'Choose a date on or before {max}.';
48
49
  this.focused = '';
49
50
  this.choosing = false;
@@ -504,6 +505,13 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
504
505
  return [{ [fault[0]]: true }, fault[1]];
505
506
  }
506
507
  }
508
+ const [start, end] = held;
509
+ if (this.mode === 'range' && start !== undefined && end !== undefined && end < start) {
510
+ return [
511
+ { rangeUnderflow: true },
512
+ formatWords(this, 'rangeOrderMessage', { start: this.spoken(start) }),
513
+ ];
514
+ }
507
515
  return undefined;
508
516
  }
509
517
  get failure() {
@@ -855,6 +863,10 @@ __decorate([
855
863
  words,
856
864
  property({ attribute: 'previous-years-label' })
857
865
  ], HelixHeadlessDatePicker.prototype, "previousYearsLabel", void 0);
866
+ __decorate([
867
+ words,
868
+ property({ attribute: 'range-order-message' })
869
+ ], HelixHeadlessDatePicker.prototype, "rangeOrderMessage", void 0);
858
870
  __decorate([
859
871
  words,
860
872
  property({ attribute: 'range-overflow-message' })
@@ -158,8 +158,17 @@ let HelixHeadlessDrilldown = class HelixHeadlessDrilldown extends MixinCollectio
158
158
  willUpdate(changed) {
159
159
  super.willUpdate(changed);
160
160
  if (changed.has('page') && (this.drilldown.page?.value ?? '') !== this.page) {
161
+ const leaving = this.drilldown.page;
162
+ const focused = this.contains(document.activeElement);
161
163
  this.showPage(this.page);
162
164
  this.page = this.drilldown.page?.value ?? '';
165
+ if (focused) {
166
+ this.updateComplete.then(() => {
167
+ const items = this.items;
168
+ const to = leaving && items.includes(leaving) ? leaving : items[0];
169
+ to?.focusItem();
170
+ });
171
+ }
163
172
  }
164
173
  hostAria(this.internals, {
165
174
  label: this.navigation ? this.label : '',
@@ -128,6 +128,19 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
128
128
  * filter goes. Set `chips` and the field adds, updates, and removes those
129
129
  * chips itself, from every `helix-facet` you do not cancel.
130
130
  *
131
+ * **The suggestions you write are an import of their own.** A suggestion is an
132
+ * option, the Option family's, which the field does not register, so a page
133
+ * that slots one imports it beside the field:
134
+ *
135
+ * ```js
136
+ * import '@fusion.dev/helix/filter-input'
137
+ * import '@fusion.dev/helix/option'
138
+ * ```
139
+ *
140
+ * In development, a field holding an option nobody registered says so once the
141
+ * page has loaded, with the line to add. The options `source` gives are drawn
142
+ * as this Option too, and need the same import.
143
+ *
131
144
  * @since 0.1.0
132
145
  * @status stable
133
146
  * @category forms
@@ -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 { AriaForwardController, countLabel, customElement, DelayController, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportRuntime, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, auditChildren, countLabel, customElement, DelayController, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportRuntime, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -552,6 +552,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
552
552
  }
553
553
  connectedCallback() {
554
554
  super.connectedCallback();
555
+ auditChildren(this, 'option', 'option', 'the field suggests nothing');
555
556
  this.addEventListener('helix-dismiss', this.handleDismiss, { capture: true });
556
557
  this.addEventListener(HelixFacetEvent.eventName, this.handleFacet);
557
558
  this.addEventListener('helix-select', this.handleSourcedSelect);
@@ -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);
@@ -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');
@@ -31,8 +31,14 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
31
31
  event.stopPropagation();
32
32
  if (this.dismiss()) {
33
33
  const next = deepContains(this, deepActiveElement()) ? this.neighbor() : null;
34
+ const holder = this.parentElement;
34
35
  this.remove();
35
36
  next?.focus();
37
+ if (next && next.getRootNode().activeElement !== next) {
38
+ void Promise.resolve()
39
+ .then(() => holder?.updateComplete)
40
+ .then(() => next.focus());
41
+ }
36
42
  }
37
43
  };
38
44
  }
@@ -154,7 +160,14 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
154
160
  ? feed.items
155
161
  : [...(this.parentElement?.children ?? [])].filter((one) => 'isNotification' in one);
156
162
  const at = rows.indexOf(this);
157
- 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);
158
171
  for (const row of order) {
159
172
  const target = row.shadowRoot?.querySelector('[part="item"]');
160
173
  if (target) {
@@ -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
@@ -4,15 +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 { AriaForwardController, afterLinkFollowed, chosenValues, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, inlineStep, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, auditChildren, chosenValues, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, inlineStep, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
- import { optionsAmong } from '../../option/root/option-root.js';
13
+ import { optionsAmong } from '../../option/among.js';
14
14
  import '../../listbox/index.js';
15
- import '../../option/index.js';
16
15
  import { over } from '../align.js';
17
16
  let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
18
17
  constructor() {
@@ -210,6 +209,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
210
209
  }
211
210
  connectedCallback() {
212
211
  super.connectedCallback();
212
+ auditChildren(this, 'option', 'option', 'it offers no choices');
213
213
  this.addEventListener('helix-select', this.handleLinkFollowed);
214
214
  const words = new MutationObserver(() => this.requestUpdate());
215
215
  words.observe(this, {
@@ -10,16 +10,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
10
10
  * `undefined` falls back to the field.
11
11
  */
12
12
  export type CellReader = (row: Readonly<Record<string, unknown>>) => unknown;
13
- /**
14
- * The edge a column names, where it names one of the three.
15
- *
16
- * **`text-align` is a string anybody can write**, the union on the property being a
17
- * compile-time claim and nothing else, and both tables put the value straight
18
- * into CSS: `Table` into a rule in each row's shadow root, `DataTable` into the
19
- * cell it draws. One guard, here with the property, rather than one in each
20
- * table and only one of them written.
21
- */
22
- export declare const edgeOf: (align: string | undefined) => string | undefined;
13
+ export { edgeOf } from '../edge.js';
23
14
  /**
24
15
  * @summary One column of a table: its heading, its width, and the field it reads from each row.
25
16
  *
@@ -10,8 +10,7 @@ import { dotsSixVertical } from '@fusion.dev/helix-icons';
10
10
  import { html, nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
12
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
- const EDGES = new Set(['center', 'end', 'start']);
14
- export const edgeOf = (align) => align && EDGES.has(align) ? align : undefined;
13
+ export { edgeOf } from '../edge.js';
15
14
  let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixElement {
16
15
  constructor() {
17
16
  super(...arguments);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The edge a column names, where it names one of the three.
3
+ *
4
+ * **`text-align` is a string anybody can write**, the union on the property being a
5
+ * compile-time claim and nothing else, and both tables put the value straight
6
+ * into CSS: `Table` into a rule in each row's shadow root, `DataTable` into the
7
+ * cell it draws. One guard, here with the property, rather than one in each
8
+ * table and only one of them written.
9
+ */
10
+ export declare const edgeOf: (align: string | undefined) => string | undefined;
@@ -0,0 +1,2 @@
1
+ const EDGES = new Set(['center', 'end', 'start']);
2
+ export const edgeOf = (align) => align && EDGES.has(align) ? align : undefined;
@@ -192,6 +192,7 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
192
192
  private refocus;
193
193
  connectedCallback(): void;
194
194
  disconnectedCallback(): void;
195
+ protected willUpdate(changed: PropertyValues): void;
195
196
  protected firstUpdated(changed: PropertyValues): void;
196
197
  /** Watches the strip's size while `scroll-controls` asks, and reads it. */
197
198
  private watchStrip;