@fusion.dev/helix-headless 0.147.0 → 0.148.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 (141) hide show
  1. package/custom-elements.json +4512 -790
  2. package/dist/accordion/item/accordion-item.d.ts +5 -2
  3. package/dist/accordion/item/accordion-item.js +11 -7
  4. package/dist/accordion/root/accordion-root.js +4 -6
  5. package/dist/announcement/root/announcement-root.d.ts +56 -5
  6. package/dist/announcement/root/announcement-root.js +124 -21
  7. package/dist/area-chart/root/area-chart-root.js +3 -4
  8. package/dist/badge/count.d.ts +14 -0
  9. package/dist/badge/count.js +20 -0
  10. package/dist/badge/root/badge-root.js +8 -22
  11. package/dist/blockquote/root/blockquote-root.d.ts +66 -8
  12. package/dist/blockquote/root/blockquote-root.js +85 -8
  13. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +2 -2
  14. package/dist/button-group/arrows.d.ts +14 -0
  15. package/dist/button-group/arrows.js +10 -0
  16. package/dist/button-group/root/button-group-root.d.ts +141 -3
  17. package/dist/button-group/root/button-group-root.js +142 -2
  18. package/dist/canvas/edge-pan.d.ts +61 -0
  19. package/dist/canvas/edge-pan.js +63 -0
  20. package/dist/canvas/node/canvas-node.d.ts +12 -2
  21. package/dist/canvas/node/canvas-node.js +6 -2
  22. package/dist/canvas/root/canvas-document.d.ts +10 -0
  23. package/dist/canvas/root/canvas-document.js +1 -0
  24. package/dist/canvas/root/canvas-root.d.ts +107 -5
  25. package/dist/canvas/root/canvas-root.js +140 -36
  26. package/dist/canvas/snap.d.ts +42 -0
  27. package/dist/canvas/snap.js +32 -0
  28. package/dist/carousel/root/carousel-root.d.ts +4 -3
  29. package/dist/carousel/root/carousel-root.js +2 -6
  30. package/dist/chat/root/chat-root.d.ts +13 -3
  31. package/dist/chat/root/chat-root.js +2 -6
  32. package/dist/checkbox-group/root/checkbox-group-root.d.ts +13 -0
  33. package/dist/checkbox-group/root/checkbox-group-root.js +18 -10
  34. package/dist/checkbox-group/select-all.d.ts +11 -0
  35. package/dist/checkbox-group/select-all.js +5 -0
  36. package/dist/code-block/root/code-block-root.d.ts +8 -6
  37. package/dist/code-block/root/code-block-root.js +4 -3
  38. package/dist/color-picker/root/color-picker-root.js +6 -12
  39. package/dist/color-picker/wheel.d.ts +11 -0
  40. package/dist/color-picker/wheel.js +9 -0
  41. package/dist/combobox/root/combobox-root.d.ts +70 -11
  42. package/dist/combobox/root/combobox-root.js +68 -18
  43. package/dist/command/root/command-root.d.ts +4 -3
  44. package/dist/data-table/root/data-table-root.js +17 -44
  45. package/dist/description-list/item/description-list-item.d.ts +4 -0
  46. package/dist/description-list/item/description-list-item.js +7 -1
  47. package/dist/divider/root/divider-root.d.ts +45 -16
  48. package/dist/divider/root/divider-root.js +28 -4
  49. package/dist/drilldown/item/drilldown-item.js +3 -4
  50. package/dist/element-events.generated.js +1 -0
  51. package/dist/empty/root/empty-root.d.ts +17 -3
  52. package/dist/empty/root/empty-root.js +29 -5
  53. package/dist/file-input/root/file-input-root.js +3 -4
  54. package/dist/file-input/status.js +2 -1
  55. package/dist/filter-input/chip/filter-chip.js +2 -3
  56. package/dist/format-date/fields.d.ts +20 -0
  57. package/dist/format-date/fields.js +14 -0
  58. package/dist/format-date/options.d.ts +2 -1
  59. package/dist/format-date/options.js +9 -38
  60. package/dist/format-date/root/format-date-root.d.ts +35 -3
  61. package/dist/format-date/root/format-date-root.js +24 -32
  62. package/dist/graph/root/graph-root.d.ts +23 -5
  63. package/dist/graph/root/graph-root.js +17 -20
  64. package/dist/heatmap/root/heatmap-root.d.ts +12 -0
  65. package/dist/heatmap/root/heatmap-root.js +26 -11
  66. package/dist/image/root/image-root.d.ts +11 -0
  67. package/dist/kbd/root/kbd-root.js +1 -2
  68. package/dist/legend/root/legend-root.js +4 -17
  69. package/dist/list/root/list-root.js +8 -22
  70. package/dist/listbox/root/listbox-root.d.ts +119 -24
  71. package/dist/listbox/root/listbox-root.js +130 -132
  72. package/dist/map/marker/map-marker.d.ts +2 -1
  73. package/dist/map/root/map-root.d.ts +40 -2
  74. package/dist/map/root/map-root.js +64 -23
  75. package/dist/megamenu/root/megamenu-root.js +4 -11
  76. package/dist/menu/root/menu-root.d.ts +2 -2
  77. package/dist/navigation/root/navigation-root.js +5 -8
  78. package/dist/notifications/group/notification-group.d.ts +8 -1
  79. package/dist/notifications/group/notification-group.js +12 -6
  80. package/dist/notifications/notification/notification.d.ts +12 -1
  81. package/dist/notifications/notification/notification.js +3 -1
  82. package/dist/notifications/root/notifications-root.d.ts +41 -5
  83. package/dist/notifications/root/notifications-root.js +27 -7
  84. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -1
  85. package/dist/object-inspector/root/object-inspector-root.js +21 -11
  86. package/dist/pagination/size/pagination-size.d.ts +1 -0
  87. package/dist/pagination/size/pagination-size.js +1 -1
  88. package/dist/pin-input/code.d.ts +23 -0
  89. package/dist/pin-input/code.js +25 -0
  90. package/dist/pin-input/root/pin-input-root.js +9 -23
  91. package/dist/qr-code/root/qr-code-root.d.ts +14 -3
  92. package/dist/qr-code/root/qr-code-root.js +19 -5
  93. package/dist/scatter-chart/measures.d.ts +22 -0
  94. package/dist/scatter-chart/measures.js +11 -0
  95. package/dist/scatter-chart/root/scatter-chart-root.d.ts +2 -6
  96. package/dist/scatter-chart/root/scatter-chart-root.js +6 -19
  97. package/dist/select/root/select-root.d.ts +56 -8
  98. package/dist/select/root/select-root.js +33 -9
  99. package/dist/speed-dial/group/speed-dial-group.d.ts +48 -0
  100. package/dist/speed-dial/group/speed-dial-group.js +43 -0
  101. package/dist/speed-dial/index.d.ts +1 -0
  102. package/dist/speed-dial/index.js +1 -0
  103. package/dist/speed-dial/item/speed-dial-item.d.ts +26 -1
  104. package/dist/speed-dial/item/speed-dial-item.js +41 -3
  105. package/dist/speed-dial/long-press.d.ts +50 -0
  106. package/dist/speed-dial/long-press.js +48 -0
  107. package/dist/speed-dial/root/speed-dial-root.d.ts +80 -1
  108. package/dist/speed-dial/root/speed-dial-root.js +114 -5
  109. package/dist/splitter/root/splitter-root.d.ts +7 -3
  110. package/dist/splitter/root/splitter-root.js +16 -3
  111. package/dist/table/cell/table-cell.d.ts +8 -1
  112. package/dist/table/cell/table-cell.js +6 -2
  113. package/dist/table/grid-controller.d.ts +117 -0
  114. package/dist/table/grid-controller.js +293 -0
  115. package/dist/table/root/table-root.d.ts +47 -3
  116. package/dist/table/root/table-root.js +81 -24
  117. package/dist/table/row/table-row.d.ts +13 -1
  118. package/dist/table/row/table-row.js +20 -6
  119. package/dist/tag/group/tag-group.js +6 -15
  120. package/dist/time-picker/root/time-picker-root.d.ts +1 -0
  121. package/dist/time-picker/root/time-picker-root.js +3 -0
  122. package/dist/toast/item/toast-item.d.ts +6 -3
  123. package/dist/toast/item/toast-item.js +1 -0
  124. package/dist/toast/root/toast-root.d.ts +13 -1
  125. package/dist/toast/root/toast-root.js +20 -14
  126. package/dist/toggle/group/toggle-group.js +3 -5
  127. package/dist/toolbar/root/toolbar-root.d.ts +31 -0
  128. package/dist/toolbar/root/toolbar-root.js +39 -8
  129. package/dist/tooltip/root/tooltip-root.js +1 -2
  130. package/dist/video/root/video-root.d.ts +11 -0
  131. package/dist/video/root/video-root.js +15 -1
  132. package/dist/virtual-list/root/virtual-list-root.js +6 -18
  133. package/lint.json +1 -1
  134. package/package.json +5 -5
  135. package/types/jsx.d.ts +2005 -266
  136. package/types/svelte.d.ts +1 -0
  137. package/types/vue.d.ts +338 -62
  138. package/dist/kbd/keys.d.ts +0 -29
  139. package/dist/kbd/keys.js +0 -52
  140. package/dist/tooltip/focus-target.d.ts +0 -6
  141. package/dist/tooltip/focus-target.js +0 -9
@@ -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 { commaList, customElement, deepActiveElement, deepContains, HelixBeforeChangeEvent, HelixElement, hostAria, KeyboardNavigationController, pressedValue, prunedValue, requestChange, rovingStop, slottedWords, typeaheadByWords, whenHydrated, } from '@fusion.dev/helix-core';
7
+ import { commaList, customElement, deepActiveElement, deepContains, focusItemOf, HelixElement, hostAria, KeyboardNavigationController, pressedValue, prunedValue, requestValue, rovingStop, slottedWords, typeaheadByWords, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessTag } from '../root/tag-root.js';
@@ -107,14 +107,11 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
107
107
  return this.tags.filter((tag) => !tag.disabled);
108
108
  }
109
109
  navigable(tag) {
110
- return {
111
- contains: (node) => tag.contains(node),
110
+ return focusItemOf(tag, {
112
111
  disabled: tag.disabled,
113
- focusItem: () => tag.focusItem(),
114
- select: () => { },
115
- selected: false,
112
+ focus: () => tag.focusItem(),
116
113
  words: tag.words,
117
- };
114
+ });
118
115
  }
119
116
  valueFor(tag) {
120
117
  return tag.value || slottedWords(tag, ['start', '']);
@@ -146,14 +143,8 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
146
143
  if (mode === 'none' || tag.disabled) {
147
144
  return;
148
145
  }
149
- requestChange({
150
- apply: (value) => {
151
- this.value = value;
152
- this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
153
- },
154
- event: (value) => new HelixBeforeChangeEvent({ value }),
155
- host: this,
156
- next: pressedValue(mode, this.written, this.valueFor(tag)),
146
+ requestValue(this, pressedValue(mode, this.written, this.valueFor(tag)), (value) => {
147
+ this.value = value;
157
148
  });
158
149
  }
159
150
  connectedCallback() {
@@ -82,6 +82,7 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
82
82
  * @csspart option-label - The time's label.
83
83
  * @csspart option-text - The label and the caption, as one column.
84
84
  *
85
+ * @cssstate blank - While no time is chosen.
85
86
  * @cssstate invalid - While `invalid` is set or `feedback` says what is wrong.
86
87
  */
87
88
  export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_base {
@@ -256,6 +256,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
256
256
  willUpdate(changed) {
257
257
  super.willUpdate(changed);
258
258
  this.report();
259
+ this.toggleState('blank', this.value === '');
259
260
  }
260
261
  updated(changed) {
261
262
  super.updated(changed);
@@ -277,6 +278,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
277
278
  return html `<${this.tag('listbox')}
278
279
  part="column"
279
280
  data-column=${part}
281
+ aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
280
282
  aria-invalid=${this.marked ? 'true' : nothing}
281
283
  exportparts="empty: column-empty, error: column-error, feedback: column-feedback, filter-icon: column-filter-icon,
282
284
  filter-mark: column-filter-mark, hint: column-hint, items: column-items, label: column-label,
@@ -328,6 +330,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
328
330
  renderList() {
329
331
  return html `<${this.tag('listbox')}
330
332
  part="list"
333
+ aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
331
334
  aria-invalid=${this.marked ? 'true' : nothing}
332
335
  exportparts="empty: list-empty, error: list-error, feedback: list-feedback, filter-icon: list-filter-icon,
333
336
  filter-mark: list-filter-mark, hint: list-hint, items: list-items, label: list-label,
@@ -10,7 +10,8 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
10
10
  *
11
11
  * **Each item runs its own timer.** A timer held by the region would restart
12
12
  * with each new message, and the last of three messages would stay for nine
13
- * seconds.
13
+ * seconds. It pauses while the pointer or focus is on the message, and while
14
+ * the page is hidden, so a message is not spent in a background tab.
14
15
  *
15
16
  * **Each message is an `alertdialog` named by its words**, which is how a reader
16
17
  * who moves into the stack hears which message they are on. Give it a
@@ -85,8 +86,10 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
85
86
  /** Writes how far a swipe has moved the message toward the end of the line. */
86
87
  private slide;
87
88
  /**
88
- * The dismiss timer, which pauses while a reader is on the message and
89
- * continues from what was left.
89
+ * The dismiss timer, which pauses while a reader is on the message, and
90
+ * while the page is hidden, and continues from what was left. A message that
91
+ * arrives in a background tab waits for the reader to come back, as React
92
+ * Aria's toast queue does.
90
93
  *
91
94
  * Paused is said on the host so the indicator can freeze with the clock. A
92
95
  * `data-` attribute rather than a property, because it is written after
@@ -35,6 +35,7 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
35
35
  done: () => {
36
36
  this.dismiss();
37
37
  },
38
+ holdWhileHidden: true,
38
39
  paused: (paused) => {
39
40
  this.toggleAttribute('data-paused', paused);
40
41
  },
@@ -106,12 +106,24 @@ export declare class HelixHeadlessToast extends HelixElement {
106
106
  /** What the assertive region is saying. */
107
107
  private urgent;
108
108
  private readonly hush;
109
+ /**
110
+ * Words waiting for the next frame: the polite ones and the urgent ones.
111
+ *
112
+ * Collected rather than written straight in, because the accessibility tree
113
+ * is built once a frame: two messages that arrive in separate tasks of one
114
+ * frame would otherwise each overwrite the region, and only the second
115
+ * would be heard.
116
+ */
117
+ private readonly pending;
118
+ /** Whether a frame is already coming to say what is pending. */
119
+ private filling;
109
120
  /**
110
121
  * Says messages that have just arrived, each once, `danger` ones assertively.
111
122
  *
112
123
  * Emptied first and filled a frame later, so a message with the same words
113
124
  * as the one before is still a change, and so a region the stack has just
114
- * shown is in the accessibility tree before its words land.
125
+ * shown is in the accessibility tree before its words land. Every message
126
+ * that arrives before that frame is said in it, each as its own sentence.
115
127
  */
116
128
  private announce;
117
129
  /**
@@ -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, DelayController, deepActiveElement, deepContains, FocusScopeController, fillTemplate, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
7
+ import { animationsFinished, customElement, DelayController, deepActiveElement, deepContains, FocusScopeController, fillTemplate, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { matchesHotkey, parseHotkey } from '../hotkey.js';
11
- import { messageWords } from '../words.js';
11
+ import { joinSaid, messageWords } from '../words.js';
12
12
  let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
13
13
  constructor() {
14
14
  super(...arguments);
@@ -17,6 +17,8 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
17
17
  this.said = '';
18
18
  this.urgent = '';
19
19
  this.hush = new DelayController(this, { renders: false });
20
+ this.pending = { said: [], urgent: [] };
21
+ this.filling = false;
20
22
  this.scope = new FocusScopeController(this, {
21
23
  contents: () => this.showing.flatMap((one) => {
22
24
  const target = focusTarget(one);
@@ -91,19 +93,25 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
91
93
  });
92
94
  }
93
95
  announce(arrived) {
94
- const urgent = arrived.filter(isDanger).map(messageWords).join(' ');
95
- const said = arrived
96
- .filter((one) => !isDanger(one))
97
- .map(messageWords)
98
- .join(' ');
99
- if (said.trim() === '' && urgent.trim() === '') {
96
+ for (const one of arrived) {
97
+ const words = messageWords(one);
98
+ if (words !== '') {
99
+ ;
100
+ (isDanger(one) ? this.pending.urgent : this.pending.said).push(words);
101
+ }
102
+ }
103
+ if (this.filling || (this.pending.said.length === 0 && this.pending.urgent.length === 0)) {
100
104
  return;
101
105
  }
106
+ this.filling = true;
102
107
  this.said = '';
103
108
  this.urgent = '';
104
109
  const fill = () => {
105
- this.said = said;
106
- this.urgent = urgent;
110
+ this.filling = false;
111
+ this.said = joinSaid(this.pending.said);
112
+ this.urgent = joinSaid(this.pending.urgent);
113
+ this.pending.said = [];
114
+ this.pending.urgent = [];
107
115
  this.hush.start(1000, () => {
108
116
  this.said = '';
109
117
  this.urgent = '';
@@ -161,8 +169,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
161
169
  continue;
162
170
  }
163
171
  this.made.delete(one);
164
- const running = typeof one.getAnimations === 'function' ? one.getAnimations({ subtree: true }) : [];
165
- void Promise.allSettled(running.map((animation) => animation.finished)).then(() => one.remove());
172
+ void animationsFinished(one).then(() => one.remove());
166
173
  }
167
174
  }
168
175
  get showing() {
@@ -194,8 +201,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
194
201
  }
195
202
  }
196
203
  lowerWhenStill(region) {
197
- const running = typeof this.getAnimations === 'function' ? this.getAnimations({ subtree: true }) : [];
198
- void Promise.allSettled(running.map((one) => one.finished)).then(() => {
204
+ void animationsFinished(this).then(() => {
199
205
  if (this.showing.length === 0 && popoverOpen(region)) {
200
206
  region.hidePopover();
201
207
  }
@@ -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 { commaList, customElement, DisabledCascadeController, HelixFormElement, hostAria, IndicatorController, KeyboardNavigationController, MixinControllerValue, MixinField, reportValues, rovingStop, toggleValue, } from '@fusion.dev/helix-core';
7
+ import { commaList, customElement, DisabledCascadeController, HelixFormElement, hostAria, IndicatorController, KeyboardNavigationController, MixinControllerValue, MixinField, reportValues, rovingStop, toggleValue, toolbarAround, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessToggle } from '../root/toggle-root.js';
@@ -139,9 +139,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
139
139
  if (!this.multiple) {
140
140
  return 'radiogroup';
141
141
  }
142
- return this.parentElement?.closest('[role=toolbar],helix-toolbar,helix-headless-toolbar')
143
- ? 'group'
144
- : 'toolbar';
142
+ return toolbarAround(this) ? 'group' : 'toolbar';
145
143
  }
146
144
  willUpdate(changed) {
147
145
  super.willUpdate(changed);
@@ -152,7 +150,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
152
150
  this.internals.setFormValue(null);
153
151
  }
154
152
  else if (this.multiple) {
155
- reportValues(this.internals, this.name, this.values);
153
+ reportValues(this.internals, this.name, this.values, { state: 'entries' });
156
154
  }
157
155
  else {
158
156
  this.internals.setFormValue(this.value);
@@ -12,6 +12,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
12
12
  * focus counts as a control, including your own elements. A child that takes no
13
13
  * focus, such as a divider, is skipped. A menu is reached at its trigger.
14
14
  *
15
+ * A Divider in the toolbar is a separator across the toolbar's axis, as React
16
+ * Aria's is: vertical in a horizontal toolbar and horizontal in a vertical
17
+ * one, without an `orientation` of its own.
18
+ *
15
19
  * Controls inside a plain wrapper, such as a `Button Group`, are each a stop
16
20
  * of the toolbar's own, as React Aria's toolbar flattens a group: the arrows
17
21
  * walk through the group's buttons and on past it, and the group takes no tab
@@ -20,6 +24,28 @@ import { type PropertyValues, type TemplateResult } from 'lit';
20
24
  * The arrows wrap from the last control to the first. Set `stop-at-ends` to
21
25
  * keep them at the ends, as Radix's `loop` set to false does.
22
26
  *
27
+ * **Keyboard.** Tab enters the toolbar on the control that last had focus, the
28
+ * first control at the start, and the next Tab leaves it.
29
+ *
30
+ * | Key | What it does |
31
+ * |---|---|
32
+ * | Right | Moves to the next control in a horizontal toolbar, from the last back to the first. |
33
+ * | Left | Moves to the control before in a horizontal toolbar, from the first round to the last. |
34
+ * | Down, Up | The same in a vertical toolbar. |
35
+ * | Home, End | Moves to the first or the last control. |
36
+ *
37
+ * Left and Right swap on a right-to-left page, where the first control is on
38
+ * the right. The arrows across the toolbar's axis are left to the control that
39
+ * has focus, so a radio-style toggle group or a select answers them itself.
40
+ * The toolbar is `role="toolbar"` with `aria-orientation`, named by `label`.
41
+ *
42
+ * The stop moves through a control's own `focusable` property where it has
43
+ * one, as Button, Select and Toggle Group do. A control without one has
44
+ * `tabindex` written on the element that takes focus: a native element
45
+ * itself, or the control inside a custom element's open shadow root, so the
46
+ * host keeps the markup you rendered. Only a custom element whose shadow root
47
+ * is closed takes it on the host.
48
+ *
23
49
  * A toggle group of pressed buttons, `selection-mode="multiple"`, is walked
24
50
  * through the same way, each toggle a stop on the toolbar's axis, as React
25
51
  * Aria's toolbar walks its toggle buttons. Another nested composite, such as a
@@ -44,6 +70,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
44
70
  * @slot - The toolbar's controls.
45
71
  *
46
72
  * @csspart toolbar - The toolbar container.
73
+ *
74
+ * @cssstate horizontal - The controls run along a row, which is the default.
75
+ * @cssstate nested - The toolbar is a group inside another toolbar, which owns the keys and the tab stop.
76
+ * @cssstate vertical - The controls run down a column, under `orientation="vertical"`.
47
77
  */
48
78
  export declare class HelixHeadlessToolbar extends HelixElement {
49
79
  /**
@@ -159,6 +189,7 @@ export declare class HelixHeadlessToolbar extends HelixElement {
159
189
  connectedCallback(): void;
160
190
  disconnectedCallback(): void;
161
191
  protected firstUpdated(changed: PropertyValues): void;
192
+ protected willUpdate(changed: PropertyValues): void;
162
193
  protected render(): TemplateResult;
163
194
  }
164
195
  declare global {
@@ -5,13 +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 HelixHeadlessToolbar_1;
8
- import { AriaForwardController, closestInstance, customElement, HelixElement, inlineStep, keyClaimed, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, closestInstance, customElement, HelixElement, inlineStep, keyClaimed, toolbarAround, } from '@fusion.dev/helix-core';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  const KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
12
12
  const ownsTabStop = (node) => 'focusable' in node;
13
13
  const NATIVE = 'a[href], button, input, select, textarea, [tabindex], [contenteditable=""], [contenteditable="true"]';
14
14
  const TABBABLE = NATIVE.replace('[tabindex]', '[tabindex]:not([tabindex="-1"])');
15
+ const innerStops = new WeakMap();
15
16
  const takesFocus = (node) => {
16
17
  if (!(node instanceof HTMLElement) ||
17
18
  node.hasAttribute('disabled') ||
@@ -21,7 +22,25 @@ const takesFocus = (node) => {
21
22
  return (node.matches(NATIVE) ||
22
23
  ownsTabStop(node) ||
23
24
  node.shadowRoot?.delegatesFocus === true ||
24
- node.shadowRoot?.querySelector(TABBABLE) != null);
25
+ node.shadowRoot?.querySelector(TABBABLE) != null ||
26
+ innerStops.get(node)?.isConnected === true);
27
+ };
28
+ const stopOf = (node) => {
29
+ if (node.hasAttribute('tabindex')) {
30
+ return node;
31
+ }
32
+ const held = innerStops.get(node);
33
+ if (held?.isConnected && held.getRootNode() === node.shadowRoot) {
34
+ return held;
35
+ }
36
+ const inner = node.localName.includes('-')
37
+ ? node.shadowRoot?.querySelector(TABBABLE)
38
+ : null;
39
+ if (!inner) {
40
+ return node;
41
+ }
42
+ innerStops.set(node, inner);
43
+ return inner;
25
44
  };
26
45
  const flattens = (node) => ownsTabStop(node) &&
27
46
  node.selectionMode === 'multiple';
@@ -58,7 +77,7 @@ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar e
58
77
  if (ownsTabStop(element)) {
59
78
  return element.focusable;
60
79
  }
61
- return composite || element.tabIndex === 0;
80
+ return composite || stopOf(element).tabIndex === 0;
62
81
  });
63
82
  if (controls.length > 0 && !held) {
64
83
  this.setTabStop(controls[0], false);
@@ -116,8 +135,9 @@ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar e
116
135
  if (element && 'focusItem' in element && typeof element.focusItem === 'function') {
117
136
  element.focusItem();
118
137
  }
119
- else {
120
- element?.focus();
138
+ else if (element) {
139
+ ;
140
+ (ownsTabStop(element) ? element : stopOf(element)).focus();
121
141
  }
122
142
  };
123
143
  }
@@ -189,7 +209,7 @@ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar e
189
209
  element.focusable = stop;
190
210
  }
191
211
  else if (foreign || this.wroteForeign) {
192
- element.tabIndex = stop ? 0 : -1;
212
+ stopOf(element).tabIndex = stop ? 0 : -1;
193
213
  }
194
214
  }
195
215
  }
@@ -206,8 +226,7 @@ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar e
206
226
  if (!parent) {
207
227
  return false;
208
228
  }
209
- return (closestInstance(parent, HelixHeadlessToolbar_1) !== null ||
210
- parent.closest('[role="toolbar"]') !== null);
229
+ return closestInstance(parent, HelixHeadlessToolbar_1) !== null || toolbarAround(this) !== null;
211
230
  }
212
231
  get axis() {
213
232
  return this.orientation ?? 'horizontal';
@@ -237,6 +256,18 @@ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar e
237
256
  super.firstUpdated(changed);
238
257
  void this.claimTabStop();
239
258
  }
259
+ willUpdate(changed) {
260
+ super.willUpdate(changed);
261
+ this.toggleState('nested', this.nested);
262
+ this.toggleState('horizontal', this.axis === 'horizontal');
263
+ this.toggleState('vertical', this.axis === 'vertical');
264
+ if (this.hasUpdated && changed.has('orientation')) {
265
+ for (const child of this.children) {
266
+ ;
267
+ child.requestUpdate?.();
268
+ }
269
+ }
270
+ }
240
271
  render() {
241
272
  const nested = this.nested;
242
273
  return html `<div
@@ -4,10 +4,9 @@ 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 { anchorElement, arrowReach, customElement, elementById, HelixElement, hostAria, linkDescription, MixinControllerOpen, OpenPhaseController, onUnlessFalse, PopoverController, reportRuntime, sideOf, unlinkDescription, } from '@fusion.dev/helix-core';
7
+ import { anchorElement, arrowReach, customElement, elementById, focusTarget, HelixElement, hostAria, linkDescription, MixinControllerOpen, OpenPhaseController, onUnlessFalse, PopoverController, reportRuntime, sideOf, unlinkDescription, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
- import { focusTarget } from '../focus-target.js';
11
10
  const GENERIC = new Set(['div', 'p', 'section', 'span']);
12
11
  const WARMTH = Symbol.for('fusion.helix.tooltip.warmth');
13
12
  const warmth = () => {
@@ -161,6 +161,15 @@ import '../../menu/index.js';
161
161
  * @cssproperty --video-intrinsic-ratio - The media's own aspect ratio, once it is known. `--video-aspect-ratio` overrides it.
162
162
  * @cssproperty --video-level - The current volume, 0 to 1.
163
163
  * @cssproperty --video-progress - How far playback has reached, as a fraction of the duration.
164
+ *
165
+ * @cssstate ended - Playback reached the end, until it starts again or the source changes.
166
+ * @cssstate error - The source could not be played, and `failure` shows.
167
+ * @cssstate fullscreen - The player fills the screen.
168
+ * @cssstate idle - The video plays and the user has been still for `idle-delay`, so the bar hides.
169
+ * @cssstate muted - The sound is off.
170
+ * @cssstate paused - Playback is paused, which is where every player starts.
171
+ * @cssstate playing - Playback runs, waiting for data included.
172
+ * @cssstate waiting - Playback stopped to wait for more data, and `loading` shows.
164
173
  */
165
174
  export declare class HelixHeadlessVideo extends HelixElement {
166
175
  /**
@@ -452,6 +461,8 @@ export declare class HelixHeadlessVideo extends HelixElement {
452
461
  */
453
462
  private failed;
454
463
  private playing;
464
+ /** Whether playback reached the end, for `:state(ended)`. */
465
+ private finished;
455
466
  private position;
456
467
  private knownDuration;
457
468
  private loudness;
@@ -72,6 +72,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
72
72
  this.buffering = false;
73
73
  this.failed = false;
74
74
  this.playing = false;
75
+ this.finished = false;
75
76
  this.position = 0;
76
77
  this.knownDuration = 0;
77
78
  this.loudness = 1;
@@ -127,6 +128,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
127
128
  };
128
129
  this.handleTime = () => {
129
130
  this.position = this.video?.currentTime ?? 0;
131
+ this.finished = this.video?.ended ?? false;
130
132
  };
131
133
  this.handleWaiting = () => {
132
134
  this.buffering = true;
@@ -144,7 +146,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
144
146
  this.handleState = (event) => {
145
147
  const video = this.video;
146
148
  this.playing = video ? !video.paused : false;
147
- if (event.type === 'emptied' || event.type === 'pause') {
149
+ this.finished = video?.ended ?? false;
150
+ if (event.type === 'emptied' || event.type === 'ended' || event.type === 'pause') {
148
151
  this.buffering = false;
149
152
  }
150
153
  if (event.type === 'play') {
@@ -438,6 +441,14 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
438
441
  super.updated?.(changed);
439
442
  this.toggleAttribute('data-playing', this.playing);
440
443
  this.toggleAttribute('data-idle', this.idle && this.playing);
444
+ this.toggleState('ended', this.finished);
445
+ this.toggleState('error', this.failed);
446
+ this.toggleState('fullscreen', this.full);
447
+ this.toggleState('idle', this.idle && this.playing);
448
+ this.toggleState('muted', this.silent);
449
+ this.toggleState('paused', !this.playing);
450
+ this.toggleState('playing', this.playing);
451
+ this.toggleState('waiting', this.buffering);
441
452
  if (changed.has('media')) {
442
453
  this.video?.load();
443
454
  }
@@ -1010,6 +1021,9 @@ __decorate([
1010
1021
  __decorate([
1011
1022
  state()
1012
1023
  ], HelixHeadlessVideo.prototype, "playing", void 0);
1024
+ __decorate([
1025
+ state()
1026
+ ], HelixHeadlessVideo.prototype, "finished", void 0);
1013
1027
  __decorate([
1014
1028
  state()
1015
1029
  ], HelixHeadlessVideo.prototype, "position", void 0);
@@ -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, commaList, customElement, elementById, HelixBeforeChangeEvent, HelixElement, HelixLoadMoreEvent, HelixRangeChangeEvent, isRtl, rangeAdd, requestChange, searchableById, selectsAll, Typeahead, toggleValue, typesCharacter, VirtualizeController, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, commaList, customElement, elementById, HelixElement, HelixLoadMoreEvent, HelixRangeChangeEvent, inRowOrder, isRtl, pressedOwnControl, rangeAdd, requestValue, searchableById, selectsAll, Typeahead, toggleValue, typesCharacter, VirtualizeController, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, render } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { holdControls, releaseControls } from '../controls.js';
@@ -98,9 +98,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
98
98
  if (at === undefined) {
99
99
  return;
100
100
  }
101
- const inner = path.slice(0, path.indexOf(row));
102
- if (inner.some((node) => node instanceof HTMLElement &&
103
- node.matches('a[href], button, input, select, textarea, label'))) {
101
+ if (pressedOwnControl(event, row, 'a[href], button, input, select, textarea, label')) {
104
102
  return;
105
103
  }
106
104
  this.press(at, event.shiftKey && this.selectionMode === 'multiple');
@@ -270,8 +268,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
270
268
  const now = commaList(this.value);
271
269
  if (extend && mode === 'multiple') {
272
270
  const keys = this.keys();
273
- const from = this.anchor === undefined ? -1 : keys.indexOf(this.anchor);
274
- const range = rangeAdd(now, keys, from, at);
271
+ const range = rangeAdd(now, keys, anchorAt(keys, this.anchor, at).anchor, at);
275
272
  if (range) {
276
273
  this.choose(this.inOrder(range));
277
274
  }
@@ -285,23 +282,14 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
285
282
  this.choose(this.inOrder(toggleValue(now, key)));
286
283
  }
287
284
  inOrder(keys) {
288
- const wanted = new Set(keys);
289
- const ordered = this.keys().filter((key) => wanted.has(key));
290
- const placed = new Set(ordered);
291
- return [...ordered, ...keys.filter((key) => !placed.has(key))].join(',');
285
+ return inRowOrder(keys, this.keys()).join(',');
292
286
  }
293
287
  choose(next) {
294
288
  if (next === this.value) {
295
289
  return;
296
290
  }
297
- requestChange({
298
- apply: (value) => {
299
- this.value = value;
300
- this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
301
- },
302
- event: (value) => new HelixBeforeChangeEvent({ value }),
303
- host: this,
304
- next,
291
+ requestValue(this, next, (value) => {
292
+ this.value = value;
305
293
  });
306
294
  }
307
295
  async focusIndex(index) {