@fusion.dev/helix-headless 0.149.1 → 0.151.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/custom-elements.json +1211 -110
  2. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  3. package/dist/area-chart/root/area-chart-root.js +17 -2
  4. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  5. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  6. package/dist/button/root/button-root.d.ts +5 -0
  7. package/dist/button/root/button-root.js +3 -0
  8. package/dist/callout/root/callout-root.d.ts +20 -0
  9. package/dist/callout/root/callout-root.js +19 -4
  10. package/dist/chat/composer/chat-composer.d.ts +30 -0
  11. package/dist/chat/composer/chat-composer.js +47 -2
  12. package/dist/chat/root/chat-root.d.ts +10 -0
  13. package/dist/chat/root/chat-root.js +14 -0
  14. package/dist/color-input/root/color-input-root.d.ts +1 -0
  15. package/dist/color-picker/root/color-picker-root.js +1 -0
  16. package/dist/combobox/root/combobox-root.d.ts +12 -1
  17. package/dist/combobox/root/combobox-root.js +8 -0
  18. package/dist/copy-button/root/copy-button-root.d.ts +4 -3
  19. package/dist/copy-button/root/copy-button-root.js +5 -1
  20. package/dist/data-table/root/data-table-root.d.ts +34 -2
  21. package/dist/data-table/root/data-table-root.js +63 -2
  22. package/dist/date-input/root/date-input-root.d.ts +1 -0
  23. package/dist/divider/root/divider-root.d.ts +8 -6
  24. package/dist/divider/root/divider-root.js +10 -2
  25. package/dist/element-events.generated.js +5 -1
  26. package/dist/filter-input/chip/filter-chip.d.ts +2 -1
  27. package/dist/filter-input/root/filter-input-root.d.ts +5 -0
  28. package/dist/filter-input/root/filter-input-root.js +7 -0
  29. package/dist/kbd/root/kbd-root.d.ts +7 -1
  30. package/dist/kbd/root/kbd-root.js +8 -4
  31. package/dist/line-chart/root/line-chart-root.d.ts +36 -0
  32. package/dist/line-chart/root/line-chart-root.js +15 -2
  33. package/dist/number-input/root/number-input-root.d.ts +6 -0
  34. package/dist/number-input/root/number-input-root.js +14 -3
  35. package/dist/option/root/option-root.d.ts +17 -8
  36. package/dist/option/root/option-root.js +18 -4
  37. package/dist/radio/group/radio-group.d.ts +8 -1
  38. package/dist/radio/group/radio-group.js +15 -1
  39. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  40. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  41. package/dist/slider/root/slider-root.d.ts +8 -0
  42. package/dist/slider/root/slider-root.js +6 -3
  43. package/dist/table/column/table-column.js +3 -2
  44. package/dist/table/grid-controller.d.ts +14 -2
  45. package/dist/table/grid-controller.js +27 -1
  46. package/dist/table/root/table-root.d.ts +30 -4
  47. package/dist/table/root/table-root.js +45 -7
  48. package/dist/table/row/table-row.d.ts +12 -2
  49. package/dist/table/row/table-row.js +8 -2
  50. package/dist/tag/group/tag-group.d.ts +22 -1
  51. package/dist/tag/group/tag-group.js +63 -3
  52. package/dist/tag/root/tag-root.d.ts +9 -0
  53. package/dist/tag/root/tag-root.js +5 -1
  54. package/dist/time-input/root/time-input-root.d.ts +1 -0
  55. package/dist/toggle/group/toggle-group.d.ts +6 -0
  56. package/dist/toggle/group/toggle-group.js +16 -1
  57. package/dist/toggle/root/toggle-root.d.ts +21 -3
  58. package/dist/toggle/root/toggle-root.js +22 -5
  59. package/dist/video/root/video-root.d.ts +6 -0
  60. package/dist/video/root/video-root.js +5 -1
  61. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  62. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  63. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  64. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  65. package/lint.json +1 -1
  66. package/package.json +5 -5
  67. package/types/jsx.d.ts +808 -156
  68. package/types/vue.d.ts +223 -59
  69. package/types/words.d.ts +4 -2
@@ -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, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, 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';
@@ -44,9 +44,12 @@ const SAMPLE = 200;
44
44
  const textOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim();
45
45
  const lengthOf = (element) => textOf(element).length;
46
46
  const numberOf = (value) => Number(value.replaceAll(',', ''));
47
+ const STATUS = new Set(['empty', 'loading']);
48
+ const NAMED = new Set([...STATUS, 'heading']);
47
49
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
48
50
  constructor() {
49
51
  super(...arguments);
52
+ this.statusSlots = true;
50
53
  this.ascendingLabel = 'ascending';
51
54
  this.canceledLabel = 'Canceled';
52
55
  this.columns = '';
@@ -82,7 +85,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
82
85
  on: () => this.grid,
83
86
  part: (name) => this.partNamed(name),
84
87
  rows: () => (this.childrenKnown ? this.arrangedRows : []),
85
- selectAll: () => this.selectable && this.requestSelected(new Set(this.everyKey)),
88
+ rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
89
+ selectAll: () => this.selectable && this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
90
+ selectRange: (to, from) => this.selectRange(to, from),
86
91
  });
87
92
  this.reorder = new ReorderController(this, {
88
93
  axis: 'block',
@@ -125,6 +130,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
125
130
  event.preventDefault();
126
131
  this.chooseRow(found.key, event.detail.checked);
127
132
  };
133
+ this.anchorKey = undefined;
128
134
  this.handleHeadClick = (event) => {
129
135
  const path = event.composedPath();
130
136
  if (!path.some((node) => node instanceof Element && node.getAttribute('part') === 'sort')) {
@@ -260,10 +266,35 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
260
266
  get everyKey() {
261
267
  return this.keyed.map(({ key }) => key);
262
268
  }
269
+ get enabledKeys() {
270
+ return this.keyed.filter(({ row }) => !row.disabled).map(({ key }) => key);
271
+ }
272
+ isDisabled(key) {
273
+ return this.keyed.find((one) => one.key === key)?.row.disabled === true;
274
+ }
263
275
  chooseAll() {
264
- this.requestSelected(new Set(toggleEvery(this.chosen, this.everyKey)));
276
+ this.requestSelected(new Set(toggleEvery(this.chosen, this.enabledKeys)));
277
+ }
278
+ selectRange(to, from) {
279
+ if (!this.selectable) {
280
+ return false;
281
+ }
282
+ const keyOf = new Map(this.keyed.map(({ key, row }) => [row, key]));
283
+ const keys = this.arrangedRows.map((row) => keyOf.get(row) ?? '');
284
+ const { anchor, anchorKey } = anchorAt(keys, this.anchorKey, from);
285
+ this.anchorKey = anchorKey;
286
+ const now = this.chosen;
287
+ const next = rangeAdd([...now], keys, anchor, to)?.filter((key) => now.has(key) || !this.isDisabled(key));
288
+ if (next && next.length > now.size) {
289
+ this.requestSelected(new Set(next));
290
+ }
291
+ return true;
265
292
  }
266
293
  chooseRow(key, on) {
294
+ if (this.isDisabled(key)) {
295
+ return;
296
+ }
297
+ this.anchorKey = key;
267
298
  const keys = this.chosen;
268
299
  if (on) {
269
300
  keys.add(key);
@@ -298,7 +329,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
298
329
  this.chooseRow(found.key, !this.chosen.has(found.key));
299
330
  return true;
300
331
  }
301
- if (event.key === 'Enter' && row?.href) {
332
+ if (event.key === 'Enter' && row?.href && !row.disabled) {
302
333
  followLink(this.ownerDocument, { href: row.href, rel: row.rel, target: row.target }, event);
303
334
  return true;
304
335
  }
@@ -418,7 +449,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
418
449
  }; }
419
450
  get slotted() {
420
451
  const columns = new Set(this.tableColumns);
421
- return [...(this.children ?? [])].filter((child) => !columns.has(child) && child.getAttribute('slot') !== 'heading');
452
+ return [...(this.children ?? [])].filter((child) => !columns.has(child) && !NAMED.has(child.getAttribute('slot') ?? ''));
422
453
  }
423
454
  assign() {
424
455
  if (this.assignsByName) {
@@ -429,7 +460,14 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
429
460
  }
430
461
  const columns = new Set(this.tableColumns);
431
462
  const headings = new Set(this.headings);
432
- assignSlots(this.renderRoot, [...columns, ...headings, ...this.arrangedRows], (child) => (columns.has(child) ? 'columns' : headings.has(child) ? 'heading' : ''));
463
+ const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
464
+ assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...this.arrangedRows], (child) => columns.has(child)
465
+ ? 'columns'
466
+ : headings.has(child)
467
+ ? 'heading'
468
+ : status.includes(child)
469
+ ? (child.getAttribute('slot') ?? '')
470
+ : '');
433
471
  }
434
472
  alignCells() {
435
473
  if (typeof CSSStyleSheet === 'undefined' || !('replaceSync' in CSSStyleSheet.prototype)) {
@@ -483,7 +521,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
483
521
  if (!this.selectable) {
484
522
  return nothing;
485
523
  }
486
- const every = this.childrenKnown ? this.keyed.map(({ key }) => key) : [];
524
+ const every = this.childrenKnown ? this.enabledKeys : [];
487
525
  const { checked, indeterminate } = everyState(this.chosen, every);
488
526
  const checkbox = this.tag('checkbox');
489
527
  const grid = this.grid;
@@ -13,7 +13,8 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
13
13
  * it, or make it interactive.
14
14
  *
15
15
  * In a `selectable` table the row draws a checkbox cell before its own cells,
16
- * and `value` is the key the table's `selected` lists it by.
16
+ * and `value` is the key the table's `selected` lists it by. `disabled` keeps
17
+ * the row out of the selection and out of its link.
17
18
  *
18
19
  * In a `reorderable` table the row draws a grip cell before those, holding a
19
20
  * button named by the table's `move-row-label`, and `value` is the key
@@ -56,7 +57,7 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
56
57
  * @cssstate dragging - The row is being moved, in a `reorderable` table.
57
58
  * @cssstate drop-after - The row being moved goes after this one when it is dropped.
58
59
  * @cssstate drop-before - The row being moved goes before this one when it is dropped.
59
- * @cssstate link - While the row has `href`, so a press on it follows the link.
60
+ * @cssstate link - While the row has `href` and is not `disabled`, so a press on it follows the link.
60
61
  * @cssstate selected - While the row is selected, in a `selectable` table.
61
62
  */
62
63
  export declare class HelixHeadlessTableRow extends HelixElement {
@@ -65,6 +66,15 @@ export declare class HelixHeadlessTableRow extends HelixElement {
65
66
  * finds its columns. Not a consumer's to read or set.
66
67
  */
67
68
  readonly isTableRow = true;
69
+ /**
70
+ * Takes the row out of the table's selection and out of its link: the row's
71
+ * checkbox is disabled, Space, Shift ranges, and the checkbox in the heading
72
+ * pass over it, and a press does not follow `href`. A row already selected
73
+ * stays selected. A screen reader hears it as unavailable, and in a `grid`
74
+ * table the arrow keys still reach it, as a disabled cell in a grid is
75
+ * reached.
76
+ */
77
+ disabled: boolean;
68
78
  /**
69
79
  * The URL a press on the row follows, which makes the whole row a link.
70
80
  * Empty, the default, leaves presses to the row's cells.
@@ -15,6 +15,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
15
15
  constructor() {
16
16
  super(...arguments);
17
17
  this.isTableRow = true;
18
+ this.disabled = false;
18
19
  this.href = '';
19
20
  this.rel = '';
20
21
  this.target = '';
@@ -30,7 +31,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
30
31
  this.selectStop = false;
31
32
  this.columnRules = undefined;
32
33
  this.handleClick = (event) => {
33
- if (this.href === '' || event.button !== 0 || event.defaultPrevented) {
34
+ if (this.disabled || this.href === '' || event.button !== 0 || event.defaultPrevented) {
34
35
  return;
35
36
  }
36
37
  const cell = event
@@ -68,8 +69,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
68
69
  super.willUpdate(changed);
69
70
  hostAria(this.internals, { role: 'row' });
70
71
  this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
72
+ this.internals.ariaDisabled = this.disabled ? 'true' : null;
71
73
  this.toggleState('selected', this.selectable && this.selected);
72
- this.toggleState('link', this.href !== '');
74
+ this.toggleState('link', this.href !== '' && !this.disabled);
73
75
  this.toggleState('dragging', this.move === 'dragging');
74
76
  this.toggleState('drop-after', this.move === 'drop-after');
75
77
  this.toggleState('drop-before', this.move === 'drop-before');
@@ -100,6 +102,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
100
102
  mark-checked: checkbox-mark-checked, mark-dash: checkbox-mark-dash,
101
103
  mark-indeterminate: checkbox-mark-indeterminate"
102
104
  aria-label=${this.selectLabel}
105
+ ?disabled=${this.disabled}
103
106
  .checked=${this.selected}
104
107
  ></${checkbox}
105
108
  ></span>`;
@@ -126,6 +129,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
126
129
  return html `${this.renderGrip()}${this.renderSelect()}<slot @slotchange=${this.handleSlotChange}></slot>`;
127
130
  }
128
131
  };
132
+ __decorate([
133
+ property({ reflect: true, type: Boolean })
134
+ ], HelixHeadlessTableRow.prototype, "disabled", void 0);
129
135
  __decorate([
130
136
  property()
131
137
  ], HelixHeadlessTableRow.prototype, "href", void 0);
@@ -15,7 +15,10 @@ export type TagSelectionMode = SelectionMode;
15
15
  * focus moves to the next tag, or the previous one after the last.
16
16
  *
17
17
  * `selection-mode` lets a reader select tags, with Space, Enter or a press:
18
- * `single` selects one at a time and `multiple` any number. `value` holds the
18
+ * `single` selects one at a time and `multiple` any number. In `multiple`,
19
+ * Shift with an arrow, Home or End selects the range from the tag last
20
+ * pressed to the tag focus moves to, and Control or Command with A selects
21
+ * every tag that is not disabled. `value` holds the
19
22
  * selected tags' values, separated by commas, and a change asks with
20
23
  * `helix-before-change` and tells with `change`. Enter on a tag with an
21
24
  * `href` follows its link. A `disabled` tag is passed over. A selected tag
@@ -30,6 +33,8 @@ export type TagSelectionMode = SelectionMode;
30
33
  * set is a `group`, and focus moves to its `empty` part,
31
34
  * which holds the `empty` slot: put words there such as "No filters" for a
32
35
  * reader to hear. With nothing slotted it says `empty-label`, "No tags".
36
+ * Where only disabled tags are left, the first of them holds the tab stop and
37
+ * takes focus, so the reader stays in the set.
33
38
  *
34
39
  * @since 0.1.0
35
40
  * @status stable
@@ -77,6 +82,12 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
77
82
  value: string | readonly string[];
78
83
  /** The tag the reader was last on, which keeps the tab stop. */
79
84
  private last;
85
+ /**
86
+ * Where a range chosen with Shift and an arrow starts, and where it reached,
87
+ * in `multiple`: the tag last pressed or moved to without Shift.
88
+ */
89
+ private anchor;
90
+ private reached;
80
91
  /**
81
92
  * The arrow keys, Home, End and typed letters, moving focus and choosing
82
93
  * nothing. From the focused tag rather than the selection, since a reader
@@ -119,6 +130,16 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
119
130
  * has moved.
120
131
  */
121
132
  protected press(tag: HelixHeadlessTag): void;
133
+ /** The reachable tags from one to the other, either way round. */
134
+ private span;
135
+ /**
136
+ * Shift and an arrow, Home or End, in `multiple`: the range from the anchor
137
+ * to the tag focus moved to is selected, in place of the range the last such
138
+ * key selected, as React Aria's selection extends.
139
+ */
140
+ private extend;
141
+ /** Asks for `value` to hold these tags' values, in their order. */
142
+ private choose;
122
143
  private readonly handleKeydown;
123
144
  private readonly handleClick;
124
145
  /**
@@ -23,7 +23,29 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
23
23
  });
24
24
  this.handleKeydown = (event) => {
25
25
  const tag = this.tagOf(event);
26
- if (!tag || tag.disabled || event.altKey || event.ctrlKey || event.metaKey) {
26
+ if (!tag || tag.disabled || event.altKey) {
27
+ return;
28
+ }
29
+ const multiple = this.selectionMode === 'multiple';
30
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
31
+ if (multiple) {
32
+ event.preventDefault();
33
+ this.choose(this.reachable);
34
+ }
35
+ return;
36
+ }
37
+ if (event.ctrlKey || event.metaKey) {
38
+ return;
39
+ }
40
+ if (MOVES.has(event.key)) {
41
+ const to = this.last;
42
+ if (multiple && event.shiftKey && to && to !== tag) {
43
+ this.extend(tag, to);
44
+ }
45
+ else if (!event.shiftKey) {
46
+ this.anchor = to;
47
+ this.reached = to;
48
+ }
27
49
  return;
28
50
  }
29
51
  if (event.key === 'Delete' || event.key === 'Backspace') {
@@ -70,7 +92,15 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
70
92
  this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
71
93
  }
72
94
  const active = document.activeElement;
73
- if (!had || this.tags.length > 0 || (active !== null && active !== document.body)) {
95
+ if (!had || (active !== null && active !== document.body)) {
96
+ return;
97
+ }
98
+ const [first] = this.tags;
99
+ if (first) {
100
+ if (this.reachable.length === 0) {
101
+ this.sync();
102
+ first.focusItem();
103
+ }
74
104
  return;
75
105
  }
76
106
  this.requestUpdate();
@@ -126,7 +156,10 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
126
156
  selected: chosen.has(this.valueFor(tag)),
127
157
  tag,
128
158
  }));
129
- const holder = rovingStop(items, items.find((item) => item.tag === this.last), { disabled: (item) => item.disabled, selected: (item) => item.selected })?.tag;
159
+ const holder = rovingStop(items, items.find((item) => item.tag === this.last), {
160
+ disabled: (item) => item.disabled,
161
+ selected: (item) => item.selected,
162
+ })?.tag ?? tags[0];
130
163
  for (const tag of tags) {
131
164
  whenHydrated(tag, () => {
132
165
  tag.focusable = tag === holder;
@@ -144,10 +177,36 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
144
177
  if (mode === 'none' || tag.disabled) {
145
178
  return;
146
179
  }
180
+ this.anchor = tag;
181
+ this.reached = tag;
147
182
  requestValue(this, pressedValue(mode, this.written, this.valueFor(tag)), (value) => {
148
183
  this.value = value;
149
184
  });
150
185
  }
186
+ span(from, to) {
187
+ const tags = this.reachable;
188
+ const [start, end] = [tags.indexOf(from), tags.indexOf(to)].sort((a, b) => a - b);
189
+ return start === undefined || end === undefined || start < 0 ? [] : tags.slice(start, end + 1);
190
+ }
191
+ extend(from, to) {
192
+ const anchor = this.anchor?.isConnected ? this.anchor : from;
193
+ const before = new Set(this.span(anchor, this.reached?.isConnected ? this.reached : from));
194
+ const after = this.span(anchor, to);
195
+ this.anchor = anchor;
196
+ this.reached = to;
197
+ const kept = this.tags.filter((tag) => commaList(this.written).includes(this.valueFor(tag)) && !before.has(tag));
198
+ const chosen = [...new Set([...kept, ...after])];
199
+ this.choose(this.tags.filter((tag) => chosen.includes(tag)));
200
+ }
201
+ choose(tags) {
202
+ const next = [...new Set(tags.map((tag) => this.valueFor(tag)))].join(', ');
203
+ if (next === this.written) {
204
+ return;
205
+ }
206
+ requestValue(this, next, (value) => {
207
+ this.value = value;
208
+ });
209
+ }
151
210
  connectedCallback() {
152
211
  super.connectedCallback();
153
212
  this.addEventListener('keydown', this.handleKeydown);
@@ -199,3 +258,4 @@ HelixHeadlessTagGroup = __decorate([
199
258
  customElement('helix-headless-tag-group')
200
259
  ], HelixHeadlessTagGroup);
201
260
  export { HelixHeadlessTagGroup };
261
+ const MOVES = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'End', 'Home']);
@@ -70,6 +70,15 @@ export declare class HelixHeadlessTag extends HelixHeadlessTag_base {
70
70
  * tag's words in it, so a translation puts them where its language does.
71
71
  */
72
72
  dismissLabel: string;
73
+ /**
74
+ * Whether the remove button is a tab stop of its own, outside a `Tag.Group`.
75
+ *
76
+ * A field that holds tags as its tokens, such as a `multiple` Combobox,
77
+ * writes `false`: the field's text is its one tab stop, and the arrow keys
78
+ * move from the text to the tags. No attribute: it is coordination between
79
+ * elements, as `focusable` is.
80
+ */
81
+ dismissTabStop: boolean;
73
82
  /**
74
83
  * Whether the tag holds its group's tab stop.
75
84
  *
@@ -12,6 +12,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
12
12
  super(...arguments);
13
13
  this.disabled = false;
14
14
  this.dismissLabel = 'Remove {label}';
15
+ this.dismissTabStop = true;
15
16
  this.href = '';
16
17
  this.value = '';
17
18
  }
@@ -87,7 +88,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
87
88
  label: this.removeName,
88
89
  mark: html `<slot name="dismiss-icon">${drawDismissGlyph()}</slot>`,
89
90
  onDismiss: () => this.dismiss(),
90
- tabindex: this.grouped ? '-1' : undefined,
91
+ tabindex: this.grouped || !this.dismissTabStop ? '-1' : undefined,
91
92
  });
92
93
  }
93
94
  };
@@ -98,6 +99,9 @@ __decorate([
98
99
  words,
99
100
  property({ attribute: 'dismiss-label' })
100
101
  ], HelixHeadlessTag.prototype, "dismissLabel", void 0);
102
+ __decorate([
103
+ property({ attribute: false })
104
+ ], HelixHeadlessTag.prototype, "dismissTabStop", void 0);
101
105
  __decorate([
102
106
  property({ attribute: false })
103
107
  ], HelixHeadlessTag.prototype, "focusable", void 0);
@@ -42,6 +42,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
42
42
  *
43
43
  * @slot feedback - The message that says what is wrong with the value.
44
44
  * @slot hint - Help under the field.
45
+ * @slot label - The label, for content richer than the `label` text.
45
46
  * @slot panel-footer - Content in the panel under the times, such as a note or an action.
46
47
  * @slot panel-header - Content in the panel above the times, such as a heading or a shortcut.
47
48
  * @slot trigger - The icon on the field that shows a list opens.
@@ -191,7 +191,13 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
191
191
  private readonly indicator;
192
192
  protected readonly keys: KeyboardNavigationController;
193
193
  protected readonly cascade: DisabledCascadeController;
194
+ /**
195
+ * The toggles, in light-DOM order, at any depth, so a toggle inside a layout
196
+ * wrapper of yours is one of the group's. A toggle held by a group nested
197
+ * inside this one is that group's.
198
+ */
194
199
  private get items();
200
+ private observer;
195
201
  private readonly handleSelection;
196
202
  /**
197
203
  * Whether the group holds an answer the form can submit: a value an item
@@ -101,7 +101,10 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
101
101
  null);
102
102
  }
103
103
  get items() {
104
- return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessToggle);
104
+ if (this.children === undefined) {
105
+ return [];
106
+ }
107
+ return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessToggle && nearestGroup(element) === this);
105
108
  }
106
109
  get answered() {
107
110
  if (this.value === '') {
@@ -118,11 +121,15 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
118
121
  super.connectedCallback();
119
122
  this.addEventListener('helix-select', this.handleSelection);
120
123
  this.addEventListener('focusin', this.noteFocus);
124
+ this.observer = new MutationObserver(this.handleItems);
125
+ this.observer.observe(this, { childList: true, subtree: true });
121
126
  }
122
127
  disconnectedCallback() {
123
128
  super.disconnectedCallback();
124
129
  this.removeEventListener('helix-select', this.handleSelection);
125
130
  this.removeEventListener('focusin', this.noteFocus);
131
+ this.observer?.disconnect();
132
+ this.observer = undefined;
126
133
  }
127
134
  firstUpdated(changed) {
128
135
  super.firstUpdated(changed);
@@ -146,6 +153,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
146
153
  this.adopt();
147
154
  hostAria(this.internals, { label: this.label, role: this.groupRole });
148
155
  this.internals.ariaOrientation = this.orientation ?? null;
156
+ this.internals.ariaRequired = this.required && !this.multiple ? 'true' : null;
149
157
  if (!this.answered) {
150
158
  this.internals.setFormValue(null);
151
159
  }
@@ -203,3 +211,10 @@ HelixHeadlessToggleGroup = __decorate([
203
211
  customElement('helix-headless-toggle-group')
204
212
  ], HelixHeadlessToggleGroup);
205
213
  export { HelixHeadlessToggleGroup };
214
+ function nearestGroup(toggle) {
215
+ let parent = toggle.parentElement;
216
+ while (parent && !(parent instanceof HelixHeadlessToggleGroup)) {
217
+ parent = parent.parentElement;
218
+ }
219
+ return parent;
220
+ }
@@ -2,13 +2,18 @@ import { HelixElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
- * @summary One option in a toggle group.
5
+ * @summary One option in a toggle group, or a toggle button on its own.
6
6
  *
7
7
  * @description
8
8
  * Renders a `<button role="radio">` for use as a form control. In a group with
9
9
  * `selection-mode="multiple"` it is a `<button>` that says whether it is
10
10
  * pressed with `aria-pressed`.
11
11
  *
12
+ * **On its own, outside a `Toggle.Group`, it is a toggle button**, as React
13
+ * Aria's `ToggleButton`: a `<button>` with `aria-pressed`. A press, Enter or
14
+ * Space sends `helix-select`, which you can cancel, then flips `selected` and
15
+ * sends `change`. Write `selected` in the markup to start it pressed.
16
+ *
12
17
  * Do not use it for navigation. For navigation, use links marked with
13
18
  * `aria-current` and styled by the `data-navigation-item` recipe in the base
14
19
  * layer. The recipe has the same look, and links are not announced as choices.
@@ -21,6 +26,7 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
21
26
  *
22
27
  * @slot - The item's label.
23
28
  *
29
+ * @event {Event} change - Fired after a toggle button on its own is pressed or released.
24
30
  * @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: call `preventDefault()` to keep the current choice.
25
31
  *
26
32
  * @csspart item - The radio button.
@@ -61,8 +67,8 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
61
67
  get defaultSelected(): boolean;
62
68
  set defaultSelected(next: boolean);
63
69
  /**
64
- * Whether the item holds the group's tab stop. Unset, the chosen item holds
65
- * it.
70
+ * Whether the item holds the group's tab stop. Unset, no group holds the
71
+ * item, and it is a toggle button with a tab stop of its own.
66
72
  *
67
73
  * Written by the group on every item: `true` for the chosen item, or for the
68
74
  * first item that can be chosen while there is no selection to carry the
@@ -79,6 +85,18 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
79
85
  * Written by the group, like `selected`, so no attribute.
80
86
  */
81
87
  pressed?: boolean | undefined;
88
+ /**
89
+ * Whether no `Toggle.Group` holds the item, which makes it a toggle button
90
+ * of its own. A group writes `focusable` on every item it holds. A server,
91
+ * and the render that hydrates its markup, cannot tell, and draw the item a
92
+ * group holds.
93
+ */
94
+ private get alone();
95
+ /**
96
+ * Chooses the item in its group, or, on its own, flips `selected` once
97
+ * `helix-select` stands and sends `change`.
98
+ */
99
+ select(): boolean;
82
100
  protected willUpdate(changed: PropertyValues): void;
83
101
  /**
84
102
  * Asks for the render that follows a hydration, so the stop stops being every
@@ -18,23 +18,40 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
18
18
  set defaultSelected(next) {
19
19
  this.toggleAttribute('selected', next);
20
20
  }
21
+ get alone() {
22
+ return this.focusable === undefined && this.childrenKnown;
23
+ }
24
+ select() {
25
+ if (!this.alone) {
26
+ return super.select();
27
+ }
28
+ if (!super.select()) {
29
+ return false;
30
+ }
31
+ this.selected = !this.selected;
32
+ this.dispatchEvent(new Event('change', { bubbles: true }));
33
+ return true;
34
+ }
21
35
  willUpdate(changed) {
22
36
  super.willUpdate(changed);
23
- this.toggleState('pressed', this.pressed === true);
37
+ this.toggleState('pressed', this.alone ? this.selected : this.pressed === true);
24
38
  }
25
39
  firstUpdated(changed) {
26
40
  super.firstUpdated(changed);
27
41
  this.finishHydration();
28
42
  }
29
43
  render() {
44
+ const alone = this.alone;
45
+ const radio = !alone && this.pressed === undefined;
46
+ const pressed = alone ? this.selected : this.pressed;
30
47
  return html `<button
31
48
  part="item"
32
49
  type="button"
33
- role=${this.pressed === undefined ? 'radio' : nothing}
34
- aria-checked=${this.pressed === undefined ? String(this.selected) : nothing}
35
- aria-pressed=${this.pressed === undefined ? nothing : String(this.pressed)}
50
+ role=${radio ? 'radio' : nothing}
51
+ aria-checked=${radio ? String(this.selected) : nothing}
52
+ aria-pressed=${radio || pressed === undefined ? nothing : String(pressed)}
36
53
  ?disabled=${this.disabled}
37
- tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
54
+ tabindex=${alone || (this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
38
55
  @click=${() => this.select()}
39
56
  ><slot></slot></button>`;
40
57
  }
@@ -316,6 +316,12 @@ export declare class HelixHeadlessVideo extends HelixElement {
316
316
  * Whether the video starts again at the end.
317
317
  */
318
318
  loop: boolean;
319
+ /**
320
+ * The title the device's own media controls show while the video plays,
321
+ * such as on a lock screen. Empty, the default, shows `label`. A playlist
322
+ * writes the current entry's name here.
323
+ */
324
+ mediaTitle: string;
319
325
  /**
320
326
  * What the button that opens `overflow-controls` is called.
321
327
  */
@@ -47,6 +47,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
47
47
  this.label = '';
48
48
  this.loadingLabel = 'Loading';
49
49
  this.loop = false;
50
+ this.mediaTitle = '';
50
51
  this.moreLabel = 'More controls';
51
52
  this.muteLabel = 'Mute';
52
53
  this.nextLabel = 'Next';
@@ -351,7 +352,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
351
352
  }
352
353
  claimSession() {
353
354
  const skips = writtenList(`${this.extraControls} ${this.overflowControls}`);
354
- this.session.claim({ artwork: this.poster, title: this.label }, {
355
+ this.session.claim({ artwork: this.poster, title: this.mediaTitle || this.label }, {
355
356
  next: skips.includes('next') ? () => this.skip(false) : null,
356
357
  previous: skips.includes('previous') ? () => this.skip(true) : null,
357
358
  seek: (seconds) => this.seek(seconds),
@@ -954,6 +955,9 @@ __decorate([
954
955
  __decorate([
955
956
  property({ reflect: true, type: Boolean })
956
957
  ], HelixHeadlessVideo.prototype, "loop", void 0);
958
+ __decorate([
959
+ property({ attribute: 'media-title' })
960
+ ], HelixHeadlessVideo.prototype, "mediaTitle", void 0);
957
961
  __decorate([
958
962
  words,
959
963
  property({ attribute: 'more-label' })
@@ -30,6 +30,10 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
30
30
  * sends `helix-end`. `shuffle` plays the entries in a random order, starting
31
31
  * from the one that is current, and `previous` retraces that order.
32
32
  *
33
+ * **The player is named by the current entry**, and the device's own media
34
+ * controls show the entry as the title. A player you gave a `label` keeps
35
+ * your name, and only the title follows the entry.
36
+ *
33
37
  * **Keyboard.** Each entry is a button in the tab order, and Enter or Space
34
38
  * plays it. The current entry carries `aria-current`. While focus is on the
35
39
  * player, the player's own keys work, and `Video` lists them. Its previous
@@ -125,7 +125,10 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
125
125
  if (player.src !== current.src) {
126
126
  player.src = current.src;
127
127
  player.poster = current.poster;
128
- player.label = current.text;
128
+ player.mediaTitle = current.text;
129
+ if (!player.hasAttribute('label')) {
130
+ player.label = current.text;
131
+ }
129
132
  if (this.chased) {
130
133
  void player.updateComplete?.then(() => player.play()).catch(() => { });
131
134
  }