@fusion.dev/helix-headless 0.146.0 → 0.147.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 (239) hide show
  1. package/custom-elements.json +5266 -785
  2. package/dist/accordion/item/accordion-item.d.ts +38 -19
  3. package/dist/accordion/item/accordion-item.js +31 -9
  4. package/dist/accordion/root/accordion-root.d.ts +8 -2
  5. package/dist/accordion/root/accordion-root.js +3 -3
  6. package/dist/announcement/root/announcement-root.d.ts +13 -4
  7. package/dist/announcement/root/announcement-root.js +34 -6
  8. package/dist/area-chart/root/area-chart-root.d.ts +18 -1
  9. package/dist/area-chart/root/area-chart-root.js +11 -3
  10. package/dist/avatar/root/avatar-root.d.ts +31 -1
  11. package/dist/avatar/root/avatar-root.js +11 -1
  12. package/dist/badge/root/badge-root.d.ts +57 -6
  13. package/dist/badge/root/badge-root.js +88 -5
  14. package/dist/bar-chart/root/bar-chart-root.js +11 -20
  15. package/dist/bar-chart/stack.d.ts +10 -0
  16. package/dist/bar-chart/stack.js +8 -0
  17. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +14 -0
  18. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +12 -2
  19. package/dist/breadcrumbs/root/breadcrumbs-root.js +2 -2
  20. package/dist/button/root/button-root.d.ts +24 -38
  21. package/dist/button/root/button-root.js +6 -37
  22. package/dist/button/submitter.d.ts +27 -0
  23. package/dist/button/submitter.js +25 -0
  24. package/dist/card/root/card-root.d.ts +12 -1
  25. package/dist/card/root/card-root.js +11 -4
  26. package/dist/chat/root/chat-root.d.ts +15 -1
  27. package/dist/chat/root/chat-root.js +11 -2
  28. package/dist/checkbox/root/checkbox-root.js +5 -7
  29. package/dist/checkbox-group/root/checkbox-group-root.js +4 -3
  30. package/dist/checkbox-group/select-all.d.ts +0 -6
  31. package/dist/checkbox-group/select-all.js +0 -5
  32. package/dist/code-block/root/code-block-root.d.ts +35 -14
  33. package/dist/code-block/root/code-block-root.js +17 -3
  34. package/dist/color-input/root/color-input-root.d.ts +11 -0
  35. package/dist/color-input/root/color-input-root.js +12 -5
  36. package/dist/color-picker/channels.js +1 -2
  37. package/dist/color-picker/root/color-picker-root.d.ts +32 -24
  38. package/dist/color-picker/root/color-picker-root.js +101 -51
  39. package/dist/color-picker/wheel.d.ts +17 -0
  40. package/dist/color-picker/wheel.js +6 -0
  41. package/dist/combobox/root/combobox-root.d.ts +13 -0
  42. package/dist/combobox/root/combobox-root.js +14 -1
  43. package/dist/command/root/command-root.d.ts +3 -1
  44. package/dist/command/root/command-root.js +2 -19
  45. package/dist/copy-button/root/copy-button-root.d.ts +16 -1
  46. package/dist/copy-button/root/copy-button-root.js +7 -2
  47. package/dist/data-table/keys.d.ts +0 -18
  48. package/dist/data-table/keys.js +1 -11
  49. package/dist/data-table/root/data-table-root.js +13 -7
  50. package/dist/date-input/root/date-input-root.d.ts +8 -1
  51. package/dist/date-input/root/date-input-root.js +8 -5
  52. package/dist/date-picker/root/date-picker-root.d.ts +14 -9
  53. package/dist/date-picker/root/date-picker-root.js +21 -16
  54. package/dist/description-list/item/description-list-item.d.ts +18 -4
  55. package/dist/description-list/item/description-list-item.js +8 -4
  56. package/dist/donut-chart/root/donut-chart-root.js +13 -6
  57. package/dist/donut-chart/slice-label.d.ts +35 -0
  58. package/dist/donut-chart/slice-label.js +11 -0
  59. package/dist/drawer/root/drawer-root.d.ts +4 -2
  60. package/dist/drawer/root/drawer-root.js +15 -3
  61. package/dist/drilldown/item/drilldown-item.d.ts +21 -1
  62. package/dist/drilldown/item/drilldown-item.js +39 -4
  63. package/dist/drilldown/root/drilldown-root.js +3 -2
  64. package/dist/element-events.generated.js +4 -1
  65. package/dist/empty/root/empty-root.d.ts +31 -1
  66. package/dist/empty/root/empty-root.js +26 -5
  67. package/dist/file-input/root/file-input-root.d.ts +34 -6
  68. package/dist/file-input/root/file-input-root.js +54 -19
  69. package/dist/file-input/status.d.ts +24 -0
  70. package/dist/file-input/status.js +10 -0
  71. package/dist/filter-input/chip/filter-chip.d.ts +23 -0
  72. package/dist/filter-input/chip/filter-chip.js +11 -2
  73. package/dist/filter-input/root/filter-input-root.d.ts +8 -0
  74. package/dist/filter-input/root/filter-input-root.js +17 -27
  75. package/dist/format-date/options.d.ts +3 -4
  76. package/dist/format-date/options.js +2 -2
  77. package/dist/format-date/root/format-date-root.d.ts +18 -1
  78. package/dist/format-date/root/format-date-root.js +19 -23
  79. package/dist/heatmap/root/buckets.d.ts +21 -0
  80. package/dist/heatmap/root/buckets.js +34 -0
  81. package/dist/heatmap/root/heatmap-root.d.ts +64 -3
  82. package/dist/heatmap/root/heatmap-root.js +89 -19
  83. package/dist/icon/index.d.ts +1 -1
  84. package/dist/icon/index.js +1 -1
  85. package/dist/icon/registry.d.ts +26 -1
  86. package/dist/icon/registry.js +12 -1
  87. package/dist/icon/root/icon-root.d.ts +13 -0
  88. package/dist/icon/root/icon-root.js +23 -2
  89. package/dist/icon/root/index.d.ts +1 -1
  90. package/dist/icon/root/index.js +1 -1
  91. package/dist/image/root/image-root.d.ts +9 -0
  92. package/dist/image/root/image-root.js +6 -1
  93. package/dist/input/root/input-root.d.ts +23 -2
  94. package/dist/input/root/input-root.js +12 -8
  95. package/dist/kbd/keys.d.ts +29 -0
  96. package/dist/kbd/keys.js +52 -0
  97. package/dist/kbd/root/kbd-root.d.ts +20 -3
  98. package/dist/kbd/root/kbd-root.js +24 -50
  99. package/dist/legend/hidden-series.d.ts +10 -2
  100. package/dist/legend/hidden-series.js +5 -2
  101. package/dist/legend/root/legend-root.d.ts +25 -7
  102. package/dist/legend/root/legend-root.js +22 -10
  103. package/dist/lightbox/root/lightbox-root.d.ts +50 -5
  104. package/dist/lightbox/root/lightbox-root.js +76 -8
  105. package/dist/lightbox/zoom.d.ts +2 -0
  106. package/dist/lightbox/zoom.js +5 -3
  107. package/dist/line-chart/root/line-chart-root.d.ts +16 -0
  108. package/dist/line-chart/root/line-chart-root.js +10 -2
  109. package/dist/list/root/list-root.d.ts +10 -2
  110. package/dist/list/root/list-root.js +26 -5
  111. package/dist/list/selection.d.ts +17 -0
  112. package/dist/list/selection.js +16 -0
  113. package/dist/listbox/root/listbox-root.d.ts +9 -5
  114. package/dist/listbox/root/listbox-root.js +29 -37
  115. package/dist/megamenu/hand-over.d.ts +75 -0
  116. package/dist/megamenu/hand-over.js +67 -0
  117. package/dist/megamenu/root/megamenu-root.d.ts +38 -14
  118. package/dist/megamenu/root/megamenu-root.js +36 -50
  119. package/dist/menu/item/menu-item.js +4 -4
  120. package/dist/menu/root/menu-root.d.ts +3 -0
  121. package/dist/meter/root/meter-root.d.ts +42 -0
  122. package/dist/meter/root/meter-root.js +33 -7
  123. package/dist/notifications/root/notifications-root.d.ts +10 -1
  124. package/dist/notifications/root/notifications-root.js +11 -1
  125. package/dist/number-input/grain.d.ts +28 -0
  126. package/dist/number-input/grain.js +14 -0
  127. package/dist/number-input/root/number-input-root.d.ts +17 -1
  128. package/dist/number-input/root/number-input-root.js +29 -18
  129. package/dist/object-inspector/root/object-inspector-root.d.ts +13 -6
  130. package/dist/object-inspector/root/object-inspector-root.js +18 -17
  131. package/dist/option/index.d.ts +1 -0
  132. package/dist/option/index.js +1 -0
  133. package/dist/option/separator/option-separator.d.ts +30 -0
  134. package/dist/option/separator/option-separator.js +21 -0
  135. package/dist/outline/root/outline-root.d.ts +12 -0
  136. package/dist/outline/root/outline-root.js +5 -19
  137. package/dist/pagination/size/pagination-size.d.ts +5 -0
  138. package/dist/pagination/size/pagination-size.js +4 -2
  139. package/dist/pin-input/code.d.ts +28 -1
  140. package/dist/pin-input/code.js +12 -0
  141. package/dist/pin-input/root/pin-input-root.d.ts +98 -121
  142. package/dist/pin-input/root/pin-input-root.js +157 -148
  143. package/dist/popover/hide-outside.js +2 -9
  144. package/dist/popover/root/popover-root.d.ts +7 -1
  145. package/dist/popover/root/popover-root.js +9 -1
  146. package/dist/progress/root/progress-root.d.ts +27 -0
  147. package/dist/progress/root/progress-root.js +25 -7
  148. package/dist/qr-code/export.d.ts +19 -0
  149. package/dist/qr-code/export.js +14 -0
  150. package/dist/qr-code/name.d.ts +11 -0
  151. package/dist/qr-code/name.js +7 -0
  152. package/dist/qr-code/root/qr-code-root.d.ts +17 -6
  153. package/dist/qr-code/root/qr-code-root.js +10 -12
  154. package/dist/radio/group/radio-group.d.ts +15 -0
  155. package/dist/radio/root/radio-root.js +2 -2
  156. package/dist/rating/root/rating-root.d.ts +22 -9
  157. package/dist/rating/root/rating-root.js +15 -39
  158. package/dist/scatter-chart/root/scatter-chart-root.d.ts +30 -0
  159. package/dist/scatter-chart/root/scatter-chart-root.js +34 -7
  160. package/dist/scroll-area/scrollbar.d.ts +0 -8
  161. package/dist/scroll-area/scrollbar.js +1 -1
  162. package/dist/select/align.d.ts +47 -0
  163. package/dist/select/align.js +7 -0
  164. package/dist/select/root/select-root.d.ts +59 -3
  165. package/dist/select/root/select-root.js +72 -12
  166. package/dist/slider/root/slider-root.d.ts +18 -1
  167. package/dist/slider/root/slider-root.js +22 -0
  168. package/dist/sparkline/readings.d.ts +30 -12
  169. package/dist/sparkline/readings.js +32 -15
  170. package/dist/sparkline/root/sparkline-root.d.ts +46 -3
  171. package/dist/sparkline/root/sparkline-root.js +60 -21
  172. package/dist/speed-dial/item/speed-dial-item.d.ts +1 -1
  173. package/dist/speed-dial/item/speed-dial-item.js +2 -2
  174. package/dist/speed-dial/root/speed-dial-root.d.ts +6 -0
  175. package/dist/speed-dial/root/speed-dial-root.js +9 -1
  176. package/dist/splitter/root/splitter-root.d.ts +3 -0
  177. package/dist/splitter/root/splitter-root.js +2 -0
  178. package/dist/steps/root/steps-root.d.ts +8 -1
  179. package/dist/steps/root/steps-root.js +6 -1
  180. package/dist/steps/step/step.d.ts +1 -0
  181. package/dist/steps/step/step.js +1 -1
  182. package/dist/table/column/table-column.d.ts +6 -0
  183. package/dist/table/column/table-column.js +5 -1
  184. package/dist/table/root/table-root.js +13 -6
  185. package/dist/table/row/table-row.d.ts +24 -0
  186. package/dist/table/row/table-row.js +37 -1
  187. package/dist/tabs/root/tabs-root.d.ts +5 -3
  188. package/dist/tag/group/tag-group.d.ts +16 -4
  189. package/dist/tag/group/tag-group.js +19 -7
  190. package/dist/textarea/root/textarea-root.d.ts +14 -2
  191. package/dist/textarea/root/textarea-root.js +6 -7
  192. package/dist/time-input/root/time-input-root.d.ts +5 -1
  193. package/dist/time-input/root/time-input-root.js +10 -7
  194. package/dist/time-picker/root/time-picker-root.d.ts +11 -14
  195. package/dist/time-picker/root/time-picker-root.js +15 -23
  196. package/dist/toast/item/toast-item.d.ts +3 -3
  197. package/dist/toast/item/toast-item.js +0 -1
  198. package/dist/toast/root/toast-root.d.ts +28 -4
  199. package/dist/toast/root/toast-root.js +58 -12
  200. package/dist/toast/words.d.ts +12 -0
  201. package/dist/toast/words.js +25 -0
  202. package/dist/toggle/group/toggle-group.d.ts +29 -5
  203. package/dist/toggle/group/toggle-group.js +26 -41
  204. package/dist/toggle/root/toggle-root.js +2 -4
  205. package/dist/toolbar/root/toolbar-root.d.ts +21 -5
  206. package/dist/toolbar/root/toolbar-root.js +52 -8
  207. package/dist/tooltip/root/tooltip-root.d.ts +7 -1
  208. package/dist/tooltip/root/tooltip-root.js +10 -1
  209. package/dist/tour/root/tour-root.d.ts +15 -0
  210. package/dist/tour/root/tour-root.js +20 -1
  211. package/dist/tour/stop/tour-stop.d.ts +3 -0
  212. package/dist/tour/stop/tour-stop.js +3 -3
  213. package/dist/tree/root/tree-root.js +2 -2
  214. package/dist/trend/figure.d.ts +16 -0
  215. package/dist/trend/figure.js +8 -0
  216. package/dist/trend/root/trend-root.d.ts +41 -2
  217. package/dist/trend/root/trend-root.js +29 -3
  218. package/dist/video/root/extra-controls.d.ts +12 -0
  219. package/dist/video/root/extra-controls.js +6 -2
  220. package/dist/video/root/video-root.d.ts +5 -1
  221. package/dist/video-playlist/root/video-playlist-root.d.ts +11 -1
  222. package/dist/video-playlist/root/video-playlist-root.js +3 -0
  223. package/dist/virtual-list/fill.d.ts +11 -0
  224. package/dist/virtual-list/fill.js +9 -0
  225. package/dist/virtual-list/keys.d.ts +16 -7
  226. package/dist/virtual-list/keys.js +17 -6
  227. package/dist/virtual-list/plan.d.ts +17 -1
  228. package/dist/virtual-list/plan.js +13 -2
  229. package/dist/virtual-list/root/virtual-list-root.d.ts +25 -3
  230. package/dist/virtual-list/root/virtual-list-root.js +48 -24
  231. package/lint.json +1 -1
  232. package/package.json +5 -5
  233. package/types/jsx.d.ts +2979 -738
  234. package/types/svelte.d.ts +1 -0
  235. package/types/vue.d.ts +619 -202
  236. package/dist/color-picker/swatches.d.ts +0 -12
  237. package/dist/color-picker/swatches.js +0 -26
  238. package/dist/rating/steps.d.ts +0 -7
  239. package/dist/rating/steps.js +0 -9
@@ -4,15 +4,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, deepActiveElement, deepContains, FocusScopeController, fillTemplate, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
7
+ import { customElement, DelayController, deepActiveElement, deepContains, FocusScopeController, fillTemplate, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  import { matchesHotkey, parseHotkey } from '../hotkey.js';
11
+ import { messageWords } from '../words.js';
11
12
  let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
12
13
  constructor() {
13
14
  super(...arguments);
14
15
  this.hotkey = 'F8';
15
16
  this.label = 'Notifications ({hotkey})';
17
+ this.said = '';
18
+ this.urgent = '';
19
+ this.hush = new DelayController(this, { renders: false });
16
20
  this.scope = new FocusScopeController(this, {
17
21
  contents: () => this.showing.flatMap((one) => {
18
22
  const target = focusTarget(one);
@@ -85,9 +89,45 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
85
89
  label: fillTemplate(this.label, { hotkey: this.hotkey }),
86
90
  role: 'region',
87
91
  });
88
- this.internals.ariaLive = 'polite';
89
- this.internals.ariaRelevant = 'additions';
90
- this.internals.ariaAtomic = 'false';
92
+ }
93
+ 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() === '') {
100
+ return;
101
+ }
102
+ this.said = '';
103
+ this.urgent = '';
104
+ const fill = () => {
105
+ this.said = said;
106
+ this.urgent = urgent;
107
+ this.hush.start(1000, () => {
108
+ this.said = '';
109
+ this.urgent = '';
110
+ });
111
+ };
112
+ if (typeof requestAnimationFrame === 'function') {
113
+ requestAnimationFrame(fill);
114
+ }
115
+ else {
116
+ fill();
117
+ }
118
+ }
119
+ tell(added) {
120
+ const showing = this.showing;
121
+ const arrived = added.filter((one) => one instanceof HTMLElement && showing.includes(one));
122
+ if (arrived.length === 0) {
123
+ return;
124
+ }
125
+ if (this.blocked()) {
126
+ this.notify(arrived);
127
+ }
128
+ else {
129
+ this.announce(arrived);
130
+ }
91
131
  }
92
132
  add(message, options = {}) {
93
133
  const tier = this.constructor.tier;
@@ -152,9 +192,6 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
152
192
  if (!open || blocked) {
153
193
  region.showPopover();
154
194
  }
155
- if (blocked) {
156
- this.notify(arrived);
157
- }
158
195
  }
159
196
  lowerWhenStill(region) {
160
197
  const running = typeof this.getAnimations === 'function' ? this.getAnimations({ subtree: true }) : [];
@@ -182,9 +219,9 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
182
219
  return;
183
220
  }
184
221
  for (const one of arrived) {
185
- const words = (one.textContent ?? '').replace(/\s+/gu, ' ').trim();
222
+ const words = messageWords(one);
186
223
  if (words !== '') {
187
- speaker.ariaNotify(words);
224
+ speaker.ariaNotify(words, { priority: isDanger(one) ? 'high' : 'normal' });
188
225
  }
189
226
  }
190
227
  }
@@ -194,7 +231,9 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
194
231
  this.addEventListener('helix-dismiss', this.handleDismiss);
195
232
  document.addEventListener('keydown', this.handleHotkey);
196
233
  this.watcher = new MutationObserver((records) => {
197
- this.place(records.flatMap((record) => [...record.addedNodes]));
234
+ const added = records.flatMap((record) => [...record.addedNodes]);
235
+ this.place(added);
236
+ this.tell(added);
198
237
  this.sweep();
199
238
  });
200
239
  this.watcher.observe(this, {
@@ -220,7 +259,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
220
259
  this.place();
221
260
  }
222
261
  render() {
223
- return html `<div part="region" popover="manual" tabindex="-1"><slot></slot></div>`;
262
+ return html `<div part="region" popover="manual" tabindex="-1"><slot></slot><span part="live" role="status">${this.said}</span><span part="live" role="alert">${this.urgent}</span></div>`;
224
263
  }
225
264
  };
226
265
  __decorate([
@@ -232,10 +271,17 @@ __decorate([
232
271
  __decorate([
233
272
  property({ reflect: true })
234
273
  ], HelixHeadlessToast.prototype, "placement", void 0);
274
+ __decorate([
275
+ state()
276
+ ], HelixHeadlessToast.prototype, "said", void 0);
277
+ __decorate([
278
+ state()
279
+ ], HelixHeadlessToast.prototype, "urgent", void 0);
235
280
  HelixHeadlessToast = __decorate([
236
281
  customElement('helix-headless-toast')
237
282
  ], HelixHeadlessToast);
238
283
  export { HelixHeadlessToast };
284
+ const isDanger = (item) => (item.tone ?? item.getAttribute('tone')) === 'danger';
239
285
  const popoverOpen = (region) => {
240
286
  try {
241
287
  return region.matches(':popover-open');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pieces of a message joined into one line a screen reader says, with a full
3
+ * stop between two where the first does not already end a sentence, so a
4
+ * heading and its detail are heard as two sentences and not run together.
5
+ */
6
+ export declare const joinSaid: (pieces: readonly string[]) => string;
7
+ /**
8
+ * What a message says as it arrives: the word for its tone, then its heading,
9
+ * its description and its text, and never its actions or its dismiss control,
10
+ * which a reader finds by moving into the message.
11
+ */
12
+ export declare const messageWords: (item: Element) => string;
@@ -0,0 +1,25 @@
1
+ import { toneMark } from '@fusion.dev/helix-core';
2
+ const UNSAID = new Set(['actions', 'dismiss-icon', 'icon']);
3
+ export const joinSaid = (pieces) => pieces
4
+ .map((piece) => piece.replace(/\s+/gu, ' ').trim())
5
+ .filter((piece) => piece !== '')
6
+ .reduce((line, piece) => line === '' ? piece : `${line}${/[.!?:;]$/u.test(line) ? '' : '.'} ${piece}`, '');
7
+ export const messageWords = (item) => {
8
+ const tone = item.tone ?? item.getAttribute('tone') ?? undefined;
9
+ const label = toneMark(tone)?.label;
10
+ const heading = [];
11
+ const rest = [];
12
+ for (const node of item.childNodes) {
13
+ if (node instanceof Element) {
14
+ const slot = node.getAttribute('slot') ?? '';
15
+ if (!UNSAID.has(slot)) {
16
+ ;
17
+ (slot === 'heading' ? heading : rest).push(node.textContent ?? '');
18
+ }
19
+ }
20
+ else if (node.nodeType === 3) {
21
+ rest.push(node.textContent ?? '');
22
+ }
23
+ }
24
+ return joinSaid([...(label ? [label] : []), ...heading, ...rest]);
25
+ };
@@ -28,6 +28,23 @@ declare const HelixHeadlessToggleGroup_base: (new (...args: any[]) => import("@f
28
28
  * `orientation="vertical"` runs the items down a column, says so to the
29
29
  * accessibility tree, and the up and down arrows move between them.
30
30
  *
31
+ * **Keyboard.** The group is one tab stop: the chosen item, or the first that
32
+ * can be chosen, as in a radio group, and in a multiple group the item focus
33
+ * was last on.
34
+ *
35
+ * | Key | What it does |
36
+ * |---|---|
37
+ * | Right, Down | Moves to the next item, from the last back to the first. In a single group, chooses it. |
38
+ * | Left, Up | Moves to the item before, from the first round to the last. In a single group, chooses it. |
39
+ * | Home, End | Moves to the first or the last item. In a single group, chooses it. |
40
+ * | Space, Enter | Chooses the focused item, or presses or releases it in a multiple group. |
41
+ *
42
+ * Left and Right swap on a right-to-left page, and a disabled item is passed
43
+ * over. A single group is a `radiogroup` of items with `role="radio"` and
44
+ * `aria-checked`. A multiple group is a `toolbar` of buttons with
45
+ * `aria-pressed`, or a `group` inside a toolbar. The group is named by
46
+ * `label` and carries `aria-orientation` under `orientation`.
47
+ *
31
48
  * @since 0.1.0
32
49
  * @status stable
33
50
  * @aliases SegmentedControl, Segmented, Navigation, Switcher
@@ -115,6 +132,11 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
115
132
  /** The item focus was last on, in a toolbar, which holds its tab stop. */
116
133
  private lastFocused;
117
134
  private readonly noteFocus;
135
+ /**
136
+ * The item whose value is the group's, in a single group: the exact match,
137
+ * so nothing chosen marks nothing. A multiple group has none.
138
+ */
139
+ private get chosenItem();
118
140
  /** Whether several items can be pressed at once. */
119
141
  private get multiple();
120
142
  /**
@@ -192,11 +214,6 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
192
214
  */
193
215
  private get groupRole();
194
216
  protected willUpdate(changed: PropertyValues): void;
195
- /**
196
- * What the form receives: the value, or under `selection-mode="multiple"`
197
- * one entry per pressed value, as a native `<select multiple>` sends.
198
- */
199
- private formValue;
200
217
  /**
201
218
  * The verdict on the answer `required` asks for.
202
219
  *
@@ -238,6 +255,13 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
238
255
  * in it.
239
256
  */
240
257
  protected render(): TemplateResult;
258
+ /**
259
+ * Items that arrive after the first render, which is every item on a page
260
+ * the parser is still reading. Outside an update here, so taking the
261
+ * declaration schedules one rather than warning, and the update reports the
262
+ * form, which a removed answer no longer holds.
263
+ */
264
+ private readonly handleItems;
241
265
  }
242
266
  declare global {
243
267
  interface HTMLElementTagNameMap {
@@ -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, rovingStop, toggleValue, } from '@fusion.dev/helix-core';
7
+ import { commaList, customElement, DisabledCascadeController, HelixFormElement, hostAria, IndicatorController, KeyboardNavigationController, MixinControllerValue, MixinField, reportValues, rovingStop, toggleValue, } 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';
@@ -13,15 +13,15 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
13
13
  super(...arguments);
14
14
  this.lastFocused = undefined;
15
15
  this.noteFocus = (event) => {
16
- const item = this.items.find((one) => one === event.target);
17
- if (this.multiple && item && item !== this.lastFocused) {
16
+ const item = event.target;
17
+ if (this.multiple && item !== this.lastFocused && this.items.includes(item)) {
18
18
  this.lastFocused = item;
19
19
  this.sync();
20
20
  }
21
21
  };
22
22
  this.focusable = true;
23
23
  this.indicator = new IndicatorController(this, {
24
- active: () => this.multiple ? undefined : this.items.find((item) => item.value === this.value),
24
+ active: () => this.chosenItem,
25
25
  indicator: () => this.partNamed('indicator'),
26
26
  observe: () => this.items,
27
27
  surface: () => this.partNamed('track'),
@@ -52,6 +52,12 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
52
52
  });
53
53
  };
54
54
  this.chosen = undefined;
55
+ this.handleItems = () => {
56
+ this.adopt();
57
+ this.sync();
58
+ this.indicator.place();
59
+ this.requestUpdate();
60
+ };
55
61
  }
56
62
  adopt() {
57
63
  if (this.value !== '' || this.children === undefined) {
@@ -63,7 +69,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
63
69
  }
64
70
  }
65
71
  sync() {
66
- const active = this.multiple ? undefined : this.items.find((item) => item.value === this.value);
72
+ const active = this.chosenItem;
67
73
  const pressed = new Set(this.values);
68
74
  const entry = this.focusable ? this.entry() : undefined;
69
75
  for (const item of this.items) {
@@ -80,9 +86,12 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
80
86
  selected: (item) => item.pressed === true,
81
87
  });
82
88
  }
83
- const active = items.find((item) => item.value === this.value);
89
+ const active = this.chosenItem;
84
90
  return active && !active.disabled ? active : items.find((item) => !item.disabled);
85
91
  }
92
+ get chosenItem() {
93
+ return this.multiple ? undefined : this.items.find((item) => item.value === this.value);
94
+ }
86
95
  get multiple() {
87
96
  return this.selectionMode === 'multiple';
88
97
  }
@@ -98,14 +107,12 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
98
107
  if (this.value === '') {
99
108
  return false;
100
109
  }
101
- if (this.multiple) {
102
- return this.children === undefined || this.values.length > 0;
103
- }
104
110
  if (this.children === undefined) {
105
111
  return true;
106
112
  }
107
- const items = this.items;
108
- return items.length === 0 || items.some((item) => item.value === this.value);
113
+ return this.multiple
114
+ ? this.values.length > 0
115
+ : this.items.length === 0 || this.chosenItem !== undefined;
109
116
  }
110
117
  connectedCallback() {
111
118
  super.connectedCallback();
@@ -132,7 +139,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
132
139
  if (!this.multiple) {
133
140
  return 'radiogroup';
134
141
  }
135
- return this.parentElement?.closest('[role="toolbar"], helix-toolbar, helix-headless-toolbar')
142
+ return this.parentElement?.closest('[role=toolbar],helix-toolbar,helix-headless-toolbar')
136
143
  ? 'group'
137
144
  : 'toolbar';
138
145
  }
@@ -141,20 +148,15 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
141
148
  this.adopt();
142
149
  hostAria(this.internals, { label: this.label, role: this.groupRole });
143
150
  this.internals.ariaOrientation = this.orientation ?? null;
144
- this.internals.setFormValue(this.formValue());
145
- }
146
- formValue() {
147
151
  if (!this.answered) {
148
- return null;
152
+ this.internals.setFormValue(null);
149
153
  }
150
- if (!this.multiple || this.name === '') {
151
- return this.value;
154
+ else if (this.multiple) {
155
+ reportValues(this.internals, this.name, this.values);
152
156
  }
153
- const data = new FormData();
154
- for (const value of this.values) {
155
- data.append(this.name, value);
157
+ else {
158
+ this.internals.setFormValue(this.value);
156
159
  }
157
- return data;
158
160
  }
159
161
  validate() {
160
162
  this.reportRequired(!this.answered, 'Choose one to continue.', 'track');
@@ -176,29 +178,12 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
176
178
  super.formStateRestoreCallback(state);
177
179
  return;
178
180
  }
179
- this.values = state.getAll(this.name).filter((one) => typeof one === 'string');
181
+ this.values = state.getAll(this.name);
180
182
  }
181
183
  render() {
182
184
  const inside = this.labelPlacement === 'start';
183
185
  const label = this.renderGroupLabel();
184
- return html `${inside ? nothing : label}
185
- <div part="row">
186
- <slot name="start"></slot>
187
- <div part="track">
188
- ${inside ? label : nothing}
189
- <div part="indicator" aria-hidden="true"></div>
190
- <slot
191
- @slotchange=${() => {
192
- this.adopt();
193
- this.sync();
194
- this.indicator.place();
195
- this.requestUpdate();
196
- }}
197
- ></slot>
198
- </div>
199
- <slot name="end"></slot>
200
- </div>
201
- ${this.renderHint()}${this.renderFeedback()}`;
186
+ return html `${inside ? nothing : label}<div part="row"><slot name="start"></slot><div part="track">${inside ? label : nothing}<div part="indicator" aria-hidden="true"></div><slot @slotchange=${this.handleItems}></slot></div><slot name="end"></slot></div>${this.renderHint()}${this.renderFeedback()}`;
202
187
  }
203
188
  };
204
189
  __decorate([
@@ -31,14 +31,12 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
31
31
  part="item"
32
32
  type="button"
33
33
  role=${this.pressed === undefined ? 'radio' : nothing}
34
- aria-checked=${this.pressed === undefined ? (this.selected ? 'true' : 'false') : nothing}
34
+ aria-checked=${this.pressed === undefined ? String(this.selected) : nothing}
35
35
  aria-pressed=${this.pressed === undefined ? nothing : String(this.pressed)}
36
36
  ?disabled=${this.disabled}
37
37
  tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
38
38
  @click=${() => this.select()}
39
- >
40
- <slot></slot>
41
- </button>`;
39
+ ><slot></slot></button>`;
42
40
  }
43
41
  };
44
42
  __decorate([
@@ -20,11 +20,20 @@ import { type PropertyValues, type TemplateResult } from 'lit';
20
20
  * The arrows wrap from the last control to the first. Set `stop-at-ends` to
21
21
  * keep them at the ends, as Radix's `loop` set to false does.
22
22
  *
23
- * A nested composite, such as a toggle group, is one stop on the toolbar's
24
- * axis: the toolbar's arrows move past it, and the arrows across the toolbar
25
- * move inside it. A composite with a `focusable` property, as Toggle Group
26
- * has, gives up its own tab stop while the toolbar's is elsewhere, so Tab
27
- * stops once in the toolbar. A composite without one keeps its own tab stop.
23
+ * A toggle group of pressed buttons, `selection-mode="multiple"`, is walked
24
+ * through the same way, each toggle a stop on the toolbar's axis, as React
25
+ * Aria's toolbar walks its toggle buttons. Another nested composite, such as a
26
+ * radio-style toggle group, is one stop on the toolbar's axis: the toolbar's
27
+ * arrows move past it, and the arrows across the toolbar move inside it. A
28
+ * composite with a `focusable` property, as Toggle Group has, gives up its own
29
+ * tab stop while the toolbar's is elsewhere, so Tab stops once in the
30
+ * toolbar. A composite without one keeps its own tab stop.
31
+ *
32
+ * A toolbar inside another toolbar, a Helix one or the page's own
33
+ * `role="toolbar"`, is a `group` of the outer one, as React Aria's is: the
34
+ * outer toolbar's arrows walk through its controls and on past them, and it
35
+ * takes no keys and no tab stop of its own. A server renders it as a toolbar,
36
+ * having no parent to read, until it upgrades.
28
37
  *
29
38
  * @since 0.1.0
30
39
  * @status stable
@@ -123,6 +132,13 @@ export declare class HelixHeadlessToolbar extends HelixElement {
123
132
  * the stop hands it to the first one left.
124
133
  */
125
134
  private readonly restoreTabStop;
135
+ /**
136
+ * Whether this toolbar sits inside another, which then owns the keys and the
137
+ * tab stop: a toolbar inside a toolbar held the reader's arrows inside it.
138
+ * Read when it renders and when it connects, there being no parent on a
139
+ * server.
140
+ */
141
+ private get nested();
126
142
  /**
127
143
  * The orientation in force.
128
144
  *
@@ -4,8 +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 { AriaForwardController, customElement, HelixElement, inlineStep, keyClaimed, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
7
+ var HelixHeadlessToolbar_1;
8
+ import { AriaForwardController, closestInstance, customElement, HelixElement, inlineStep, keyClaimed, } from '@fusion.dev/helix-core';
9
+ import { html, nothing } from 'lit';
9
10
  import { property } from 'lit/decorators.js';
10
11
  const KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
11
12
  const ownsTabStop = (node) => 'focusable' in node;
@@ -22,7 +23,9 @@ const takesFocus = (node) => {
22
23
  node.shadowRoot?.delegatesFocus === true ||
23
24
  node.shadowRoot?.querySelector(TABBABLE) != null);
24
25
  };
25
- let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
26
+ const flattens = (node) => ownsTabStop(node) &&
27
+ node.selectionMode === 'multiple';
28
+ let HelixHeadlessToolbar = HelixHeadlessToolbar_1 = class HelixHeadlessToolbar extends HelixElement {
26
29
  constructor() {
27
30
  super(...arguments);
28
31
  this.label = 'Toolbar';
@@ -33,14 +36,23 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
33
36
  this.wroteForeign = false;
34
37
  this.stopAtEnds = false;
35
38
  this.handleFocusIn = (event) => {
39
+ if (this.nested) {
40
+ return;
41
+ }
36
42
  const control = this.origin(event);
37
43
  if (control) {
38
44
  this.setTabStop(control);
39
45
  }
40
46
  };
41
47
  this.restoreTabStop = () => {
48
+ if (this.nested) {
49
+ return;
50
+ }
42
51
  const controls = this.controls;
43
52
  const held = controls.some((control) => {
53
+ if (control.group) {
54
+ return control.group.focusable;
55
+ }
44
56
  const composite = control.stops.length !== 1;
45
57
  const element = composite ? control.child : control.stops[0];
46
58
  if (ownsTabStop(element)) {
@@ -53,8 +65,11 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
53
65
  }
54
66
  };
55
67
  this.handleCompositeKeydown = (event) => {
68
+ if (this.nested) {
69
+ return;
70
+ }
56
71
  const control = this.origin(event);
57
- if (!control || control.stops.length === 1) {
72
+ if (!control || (control.stops.length === 1 && !control.group)) {
58
73
  return;
59
74
  }
60
75
  this.handleKeydown(event);
@@ -63,7 +78,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
63
78
  }
64
79
  };
65
80
  this.handleKeydown = (event) => {
66
- if (!KEYS.has(event.key) || keyClaimed(event)) {
81
+ if (this.nested || !KEYS.has(event.key) || keyClaimed(event)) {
67
82
  return;
68
83
  }
69
84
  const along = inlineStep(this, event.key);
@@ -115,7 +130,14 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
115
130
  continue;
116
131
  }
117
132
  if (takesFocus(child)) {
118
- controls.push({ child, stops: this.inside(child) });
133
+ const stops = this.inside(child);
134
+ if (stops.length > 1 && flattens(child)) {
135
+ for (const stop of stops) {
136
+ controls.push({ child: stop, group: child, stops: [stop] });
137
+ }
138
+ continue;
139
+ }
140
+ controls.push({ child, stops });
119
141
  continue;
120
142
  }
121
143
  for (const stop of this.within(child)) {
@@ -151,6 +173,10 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
151
173
  this.wroteForeign = true;
152
174
  }
153
175
  for (const control of this.controls) {
176
+ if (control.group) {
177
+ control.group.focusable = control.group === active?.group;
178
+ continue;
179
+ }
154
180
  if (control.stops.length !== 1) {
155
181
  if (ownsTabStop(control.child)) {
156
182
  control.child.focusable = control.child === active?.child;
@@ -168,15 +194,27 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
168
194
  }
169
195
  }
170
196
  async claimTabStop() {
197
+ if (this.nested) {
198
+ return;
199
+ }
171
200
  const pending = [...new Set([...this.children].map((child) => child.localName))].filter((name) => name.includes('-'));
172
201
  await Promise.all(pending.map((name) => customElements.whenDefined(name)));
173
202
  this.setTabStop(this.controls[0], false);
174
203
  }
204
+ get nested() {
205
+ const parent = this.parentElement;
206
+ if (!parent) {
207
+ return false;
208
+ }
209
+ return (closestInstance(parent, HelixHeadlessToolbar_1) !== null ||
210
+ parent.closest('[role="toolbar"]') !== null);
211
+ }
175
212
  get axis() {
176
213
  return this.orientation ?? 'horizontal';
177
214
  }
178
215
  connectedCallback() {
179
216
  super.connectedCallback();
217
+ this.requestUpdate();
180
218
  this.addEventListener('keydown', this.handleCompositeKeydown, true);
181
219
  this.addEventListener('keydown', this.handleKeydown);
182
220
  this.addEventListener('focusin', this.handleFocusIn);
@@ -200,7 +238,13 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
200
238
  void this.claimTabStop();
201
239
  }
202
240
  render() {
203
- return html `<div part="toolbar" role="toolbar" aria-label=${this.label} aria-orientation=${this.axis}>
241
+ const nested = this.nested;
242
+ return html `<div
243
+ part="toolbar"
244
+ role=${nested ? 'group' : 'toolbar'}
245
+ aria-label=${this.label}
246
+ aria-orientation=${nested ? nothing : this.axis}
247
+ >
204
248
  <slot @slotchange=${() => this.claimTabStop()}></slot>
205
249
  </div>`;
206
250
  }
@@ -214,7 +258,7 @@ __decorate([
214
258
  __decorate([
215
259
  property({ attribute: 'stop-at-ends', type: Boolean })
216
260
  ], HelixHeadlessToolbar.prototype, "stopAtEnds", void 0);
217
- HelixHeadlessToolbar = __decorate([
261
+ HelixHeadlessToolbar = HelixHeadlessToolbar_1 = __decorate([
218
262
  customElement('helix-headless-toolbar')
219
263
  ], HelixHeadlessToolbar);
220
264
  export { HelixHeadlessToolbar };
@@ -1,4 +1,4 @@
1
- import { HelixElement, type Placement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, OpenPhaseController, type Placement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
4
4
  /**
@@ -27,6 +27,9 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
27
27
  * `aria-describedby` pointing at the tooltip yourself to have the link in the
28
28
  * server's markup. Where the browser has no element references, write it
29
29
  * yourself; the tooltip warns once on first open if nothing links the two.
30
+ * In the `for` form the tooltip itself is out of the accessibility tree while
31
+ * its bubble is down, so a reader walking the page meets its words once, as
32
+ * the trigger's description.
30
33
  *
31
34
  * The tooltip shows on hover and on focus. It hides on pointer leave, on blur,
32
35
  * on Escape, and when the trigger is activated. The pointer can move from the
@@ -65,6 +68,8 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
65
68
  * @csspart tooltip-right - Added to `tooltip` while the bubble is right of its trigger.
66
69
  * @csspart tooltip-top - Added to `tooltip` while the bubble is above its trigger.
67
70
  *
71
+ * @cssstate entering - From the moment the bubble shows until its entrance has finished.
72
+ * @cssstate exiting - From the moment the bubble hides until its exit has finished.
68
73
  * @cssstate open - While the tooltip shows.
69
74
  * @cssstate placed-bottom - While the bubble sits below its trigger.
70
75
  * @cssstate placed-left - While the bubble sits to the left of its trigger.
@@ -236,6 +241,7 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
236
241
  * it must stay inside, and a component often has one and not the other.
237
242
  */
238
243
  private get enclosure();
244
+ protected readonly phases: OpenPhaseController;
239
245
  private get bubble();
240
246
  /**
241
247
  * Whether there is anything to show.
@@ -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 { anchorElement, arrowReach, customElement, elementById, HelixElement, hostAria, linkDescription, MixinControllerOpen, onUnlessFalse, PopoverController, reportRuntime, sideOf, unlinkDescription, } from '@fusion.dev/helix-core';
7
+ import { anchorElement, arrowReach, customElement, elementById, 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
10
  import { focusTarget } from '../focus-target.js';
@@ -51,6 +51,14 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
51
51
  surface: () => this.partNamed('tooltip'),
52
52
  within: () => this.enclosure,
53
53
  });
54
+ this.phases = new OpenPhaseController(this, {
55
+ mark: {
56
+ entering: (on) => this.toggleState('entering', on),
57
+ exiting: (on) => this.toggleState('exiting', on),
58
+ },
59
+ open: () => this.open,
60
+ surface: () => this.partNamed('tooltip'),
61
+ });
54
62
  this.handleContentChange = () => {
55
63
  this.requestUpdate();
56
64
  };
@@ -335,6 +343,7 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
335
343
  describedBy: this.hasContent ? [bubble] : [],
336
344
  label: this.describedText,
337
345
  });
346
+ this.internals.ariaHidden = this.for !== '' && !this.open ? 'true' : null;
338
347
  }
339
348
  render() {
340
349
  const side = this.side;