@fusion.dev/helix-headless 0.142.1 → 0.144.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 (285) hide show
  1. package/custom-elements.json +25381 -1202
  2. package/dist/accordion/item/accordion-item.d.ts +38 -2
  3. package/dist/accordion/item/accordion-item.js +38 -6
  4. package/dist/accordion/root/accordion-root.d.ts +23 -1
  5. package/dist/accordion/root/accordion-root.js +39 -3
  6. package/dist/announcement/root/announcement-root.d.ts +11 -7
  7. package/dist/announcement/root/announcement-root.js +1 -1
  8. package/dist/app-header/root/app-header-root.d.ts +21 -0
  9. package/dist/app-header/root/app-header-root.js +31 -5
  10. package/dist/area-chart/root/area-chart-root.d.ts +12 -4
  11. package/dist/area-chart/root/area-chart-root.js +39 -11
  12. package/dist/avatar/group/avatar-group.d.ts +24 -3
  13. package/dist/avatar/group/avatar-group.js +34 -2
  14. package/dist/avatar/root/avatar-root.d.ts +17 -1
  15. package/dist/avatar/root/avatar-root.js +31 -10
  16. package/dist/badge/root/badge-root.d.ts +12 -0
  17. package/dist/badge/root/badge-root.js +1 -1
  18. package/dist/bar-chart/root/bar-chart-root.d.ts +30 -5
  19. package/dist/bar-chart/root/bar-chart-root.js +58 -21
  20. package/dist/blockquote/root/blockquote-root.d.ts +12 -7
  21. package/dist/blockquote/root/blockquote-root.js +6 -4
  22. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +9 -3
  23. package/dist/breadcrumbs/root/breadcrumbs-root.js +9 -1
  24. package/dist/button/root/button-root.d.ts +84 -1
  25. package/dist/button/root/button-root.js +81 -3
  26. package/dist/callout/root/callout-root.d.ts +32 -6
  27. package/dist/callout/root/callout-root.js +26 -7
  28. package/dist/canvas/root/canvas-root.d.ts +1 -0
  29. package/dist/canvas/root/canvas-root.js +10 -1
  30. package/dist/card/root/card-root.d.ts +30 -1
  31. package/dist/card/root/card-root.js +38 -5
  32. package/dist/carousel/root/carousel-root.d.ts +47 -5
  33. package/dist/carousel/root/carousel-root.js +53 -21
  34. package/dist/chat/composer/chat-composer.d.ts +13 -11
  35. package/dist/chat/composer/chat-composer.js +6 -12
  36. package/dist/chat/message/chat-message.js +8 -1
  37. package/dist/chat/root/chat-root.d.ts +1 -0
  38. package/dist/chat/root/chat-root.js +2 -1
  39. package/dist/checkbox/root/checkbox-root.d.ts +7 -0
  40. package/dist/checkbox/root/checkbox-root.js +9 -3
  41. package/dist/checkbox-group/root/checkbox-group-root.d.ts +116 -33
  42. package/dist/checkbox-group/root/checkbox-group-root.js +141 -44
  43. package/dist/code/root/code-root.d.ts +1 -1
  44. package/dist/code-block/lines.d.ts +13 -0
  45. package/dist/code-block/lines.js +2 -0
  46. package/dist/code-block/root/code-block-root.d.ts +49 -0
  47. package/dist/code-block/root/code-block-root.js +44 -3
  48. package/dist/color-input/root/color-input-root.d.ts +60 -2
  49. package/dist/color-input/root/color-input-root.js +55 -7
  50. package/dist/color-picker/channels.d.ts +26 -0
  51. package/dist/color-picker/channels.js +84 -0
  52. package/dist/color-picker/root/color-picker-root.d.ts +118 -12
  53. package/dist/color-picker/root/color-picker-root.js +183 -13
  54. package/dist/combobox/root/combobox-root.d.ts +126 -3
  55. package/dist/combobox/root/combobox-root.js +224 -21
  56. package/dist/command/root/command-root.d.ts +47 -10
  57. package/dist/command/root/command-root.js +45 -4
  58. package/dist/copy-button/from.d.ts +22 -0
  59. package/dist/copy-button/from.js +28 -0
  60. package/dist/copy-button/root/copy-button-root.d.ts +48 -7
  61. package/dist/copy-button/root/copy-button-root.js +54 -8
  62. package/dist/data-table/root/data-table-root.d.ts +164 -10
  63. package/dist/data-table/root/data-table-root.js +488 -57
  64. package/dist/date-input/root/date-input-root.d.ts +123 -9
  65. package/dist/date-input/root/date-input-root.js +147 -45
  66. package/dist/date-picker/root/date-picker-root.d.ts +229 -18
  67. package/dist/date-picker/root/date-picker-root.js +342 -46
  68. package/dist/dialog/description.d.ts +9 -0
  69. package/dist/dialog/description.js +14 -0
  70. package/dist/dialog/root/dialog-root.d.ts +101 -10
  71. package/dist/dialog/root/dialog-root.js +83 -4
  72. package/dist/donut-chart/root/donut-chart-root.d.ts +61 -16
  73. package/dist/donut-chart/root/donut-chart-root.js +48 -13
  74. package/dist/drawer/root/drawer-root.d.ts +93 -7
  75. package/dist/drawer/root/drawer-root.js +126 -5
  76. package/dist/drawer/swipe-start.d.ts +9 -0
  77. package/dist/drawer/swipe-start.js +12 -0
  78. package/dist/drilldown/item/drilldown-item.d.ts +16 -13
  79. package/dist/drilldown/item/drilldown-item.js +6 -2
  80. package/dist/drilldown/root/drilldown-root.d.ts +21 -5
  81. package/dist/drilldown/root/drilldown-root.js +26 -10
  82. package/dist/element-events.generated.js +42 -13
  83. package/dist/empty/root/empty-root.d.ts +1 -1
  84. package/dist/file-input/root/file-input-root.d.ts +65 -1
  85. package/dist/file-input/root/file-input-root.js +76 -12
  86. package/dist/file-input/screen.d.ts +35 -0
  87. package/dist/file-input/screen.js +49 -0
  88. package/dist/filter-input/menu/filter-menu.d.ts +9 -0
  89. package/dist/filter-input/menu/filter-menu.js +8 -0
  90. package/dist/filter-input/root/filter-input-root.d.ts +43 -6
  91. package/dist/filter-input/root/filter-input-root.js +89 -10
  92. package/dist/filter-input/suggest.d.ts +17 -0
  93. package/dist/filter-input/suggest.js +23 -0
  94. package/dist/format-date/options.d.ts +33 -0
  95. package/dist/format-date/options.js +42 -0
  96. package/dist/format-date/root/format-date-root.d.ts +62 -3
  97. package/dist/format-date/root/format-date-root.js +78 -37
  98. package/dist/graph/root/graph-root.d.ts +24 -3
  99. package/dist/graph/root/graph-root.js +66 -12
  100. package/dist/heatmap/root/buckets.d.ts +2 -1
  101. package/dist/heatmap/root/buckets.js +5 -1
  102. package/dist/heatmap/root/heatmap-root.d.ts +47 -9
  103. package/dist/heatmap/root/heatmap-root.js +46 -18
  104. package/dist/icon/index.d.ts +2 -0
  105. package/dist/icon/index.js +2 -0
  106. package/dist/icon/registry.d.ts +37 -0
  107. package/dist/icon/registry.js +83 -0
  108. package/dist/icon/root/icon-root.d.ts +53 -6
  109. package/dist/icon/root/icon-root.js +72 -4
  110. package/dist/icon/root/index.d.ts +2 -0
  111. package/dist/icon/root/index.js +2 -0
  112. package/dist/icon/set.d.ts +1 -0
  113. package/dist/icon/set.js +3 -0
  114. package/dist/icon-button/root/icon-button-root.d.ts +1 -1
  115. package/dist/image/root/image-root.d.ts +23 -2
  116. package/dist/image/root/image-root.js +23 -4
  117. package/dist/input/root/input-root.d.ts +62 -9
  118. package/dist/input/root/input-root.js +61 -5
  119. package/dist/kbd/root/kbd-root.d.ts +26 -11
  120. package/dist/kbd/root/kbd-root.js +35 -38
  121. package/dist/kbd/shortcut.d.ts +18 -0
  122. package/dist/kbd/shortcut.js +35 -0
  123. package/dist/legend/hidden-series.d.ts +8 -0
  124. package/dist/legend/hidden-series.js +13 -0
  125. package/dist/legend/root/legend-root.d.ts +63 -0
  126. package/dist/legend/root/legend-root.js +61 -7
  127. package/dist/lightbox/item/lightbox-item.d.ts +17 -1
  128. package/dist/lightbox/item/lightbox-item.js +10 -1
  129. package/dist/lightbox/root/lightbox-root.d.ts +77 -6
  130. package/dist/lightbox/root/lightbox-root.js +116 -5
  131. package/dist/line-chart/root/line-chart-root.d.ts +29 -4
  132. package/dist/line-chart/root/line-chart-root.js +58 -17
  133. package/dist/list/item/list-item.d.ts +11 -5
  134. package/dist/list/item/list-item.js +5 -6
  135. package/dist/listbox/range.d.ts +17 -0
  136. package/dist/listbox/range.js +5 -0
  137. package/dist/listbox/root/listbox-root.d.ts +184 -4
  138. package/dist/listbox/root/listbox-root.js +229 -23
  139. package/dist/loader/busy.d.ts +17 -0
  140. package/dist/loader/busy.js +24 -0
  141. package/dist/loader/root/loader-root.d.ts +37 -0
  142. package/dist/loader/root/loader-root.js +52 -3
  143. package/dist/map/root/map-root.d.ts +19 -4
  144. package/dist/map/root/map-root.js +36 -7
  145. package/dist/megamenu/root/megamenu-root.d.ts +10 -1
  146. package/dist/megamenu/root/megamenu-root.js +10 -1
  147. package/dist/menu/item/menu-item.d.ts +41 -12
  148. package/dist/menu/item/menu-item.js +46 -3
  149. package/dist/menu/root/menu-root.d.ts +32 -2
  150. package/dist/menu/root/menu-root.js +48 -8
  151. package/dist/meter/root/meter-root.d.ts +13 -0
  152. package/dist/meter/root/meter-root.js +24 -5
  153. package/dist/navigation/root/navigation-root.d.ts +10 -2
  154. package/dist/navigation/root/navigation-root.js +13 -2
  155. package/dist/notifications/feed-keys.d.ts +10 -0
  156. package/dist/notifications/feed-keys.js +14 -0
  157. package/dist/notifications/group/notification-group.d.ts +1 -0
  158. package/dist/notifications/notification/notification.d.ts +29 -0
  159. package/dist/notifications/notification/notification.js +51 -3
  160. package/dist/notifications/root/notifications-root.d.ts +49 -0
  161. package/dist/notifications/root/notifications-root.js +76 -2
  162. package/dist/number-input/root/number-input-root.d.ts +109 -7
  163. package/dist/number-input/root/number-input-root.js +109 -21
  164. package/dist/object-inspector/root/object-inspector-root.d.ts +49 -5
  165. package/dist/object-inspector/root/object-inspector-root.js +92 -12
  166. package/dist/option/root/option-root.d.ts +58 -6
  167. package/dist/option/root/option-root.js +35 -5
  168. package/dist/outline/root/outline-root.d.ts +13 -2
  169. package/dist/outline/root/outline-root.js +4 -2
  170. package/dist/pagination/root/pagination-root.d.ts +19 -1
  171. package/dist/pagination/root/pagination-root.js +49 -9
  172. package/dist/pagination/size/pagination-size.d.ts +13 -3
  173. package/dist/pagination/size/pagination-size.js +7 -0
  174. package/dist/pin-input/format.d.ts +7 -0
  175. package/dist/pin-input/format.js +16 -0
  176. package/dist/pin-input/root/pin-input-root.d.ts +37 -0
  177. package/dist/pin-input/root/pin-input-root.js +48 -7
  178. package/dist/popover/root/popover-root.d.ts +78 -3
  179. package/dist/popover/root/popover-root.js +74 -5
  180. package/dist/progress/root/progress-root.d.ts +23 -1
  181. package/dist/progress/root/progress-root.js +40 -10
  182. package/dist/qr-code/export.d.ts +31 -0
  183. package/dist/qr-code/export.js +10 -0
  184. package/dist/qr-code/root/qr-code-root.d.ts +45 -0
  185. package/dist/qr-code/root/qr-code-root.js +78 -3
  186. package/dist/radio/group/radio-group.d.ts +64 -0
  187. package/dist/radio/group/radio-group.js +66 -12
  188. package/dist/radio/root/radio-root.d.ts +8 -5
  189. package/dist/radio/root/radio-root.js +1 -2
  190. package/dist/rating/root/rating-root.d.ts +49 -1
  191. package/dist/rating/root/rating-root.js +69 -10
  192. package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
  193. package/dist/scatter-chart/root/scatter-chart-root.js +40 -14
  194. package/dist/select/root/select-root.d.ts +93 -3
  195. package/dist/select/root/select-root.js +111 -13
  196. package/dist/skeleton/root/skeleton-root.d.ts +39 -3
  197. package/dist/skeleton/root/skeleton-root.js +33 -4
  198. package/dist/skip-link/root/skip-link-root.d.ts +10 -0
  199. package/dist/skip-link/root/skip-link-root.js +15 -1
  200. package/dist/slider/root/slider-root.d.ts +59 -2
  201. package/dist/slider/root/slider-root.js +82 -8
  202. package/dist/sparkline/root/sparkline-root.d.ts +9 -10
  203. package/dist/speed-dial/item/speed-dial-item.d.ts +11 -5
  204. package/dist/speed-dial/item/speed-dial-item.js +7 -2
  205. package/dist/speed-dial/root/speed-dial-root.d.ts +15 -4
  206. package/dist/speed-dial/root/speed-dial-root.js +19 -5
  207. package/dist/splitter/root/splitter-root.d.ts +28 -6
  208. package/dist/splitter/root/splitter-root.js +35 -1
  209. package/dist/stat/root/stat-root.d.ts +9 -1
  210. package/dist/stat/root/stat-root.js +15 -2
  211. package/dist/steps/root/steps-root.js +1 -1
  212. package/dist/steps/step/step.d.ts +1 -0
  213. package/dist/steps/step/step.js +1 -1
  214. package/dist/switch/root/switch-root.d.ts +14 -2
  215. package/dist/switch/root/switch-root.js +7 -1
  216. package/dist/table/cell/table-cell.d.ts +24 -4
  217. package/dist/table/cell/table-cell.js +21 -1
  218. package/dist/table/column/table-column.d.ts +32 -6
  219. package/dist/table/column/table-column.js +36 -6
  220. package/dist/table/root/table-root.d.ts +94 -3
  221. package/dist/table/root/table-root.js +128 -5
  222. package/dist/table/row/table-row.d.ts +7 -0
  223. package/dist/table/row/table-row.js +9 -1
  224. package/dist/tabs/root/tabs-root.d.ts +6 -1
  225. package/dist/tabs/root/tabs-root.js +6 -2
  226. package/dist/tag/group/tag-group.d.ts +107 -0
  227. package/dist/tag/group/tag-group.js +157 -0
  228. package/dist/tag/index.d.ts +1 -0
  229. package/dist/tag/index.js +1 -0
  230. package/dist/tag/root/tag-root.d.ts +83 -1
  231. package/dist/tag/root/tag-root.js +87 -17
  232. package/dist/tag/selection.d.ts +10 -0
  233. package/dist/tag/selection.js +12 -0
  234. package/dist/textarea/grow.d.ts +22 -0
  235. package/dist/textarea/grow.js +20 -0
  236. package/dist/textarea/root/textarea-root.d.ts +13 -0
  237. package/dist/textarea/root/textarea-root.js +10 -2
  238. package/dist/time-input/root/time-input-root.d.ts +71 -6
  239. package/dist/time-input/root/time-input-root.js +110 -44
  240. package/dist/time-picker/root/time-picker-root.d.ts +155 -26
  241. package/dist/time-picker/root/time-picker-root.js +192 -63
  242. package/dist/toast/hotkey.d.ts +22 -0
  243. package/dist/toast/hotkey.js +23 -0
  244. package/dist/toast/item/toast-item.d.ts +16 -1
  245. package/dist/toast/item/toast-item.js +41 -13
  246. package/dist/toast/root/toast-root.d.ts +105 -8
  247. package/dist/toast/root/toast-root.js +209 -4
  248. package/dist/toggle/group/toggle-group.d.ts +95 -16
  249. package/dist/toggle/group/toggle-group.js +99 -13
  250. package/dist/toggle/root/toggle-root.d.ts +26 -11
  251. package/dist/toggle/root/toggle-root.js +14 -6
  252. package/dist/toolbar/root/toolbar-root.d.ts +55 -15
  253. package/dist/toolbar/root/toolbar-root.js +99 -16
  254. package/dist/tooltip/focus-target.d.ts +6 -0
  255. package/dist/tooltip/focus-target.js +9 -0
  256. package/dist/tooltip/root/tooltip-root.d.ts +73 -15
  257. package/dist/tooltip/root/tooltip-root.js +96 -7
  258. package/dist/tour/naming.d.ts +30 -0
  259. package/dist/tour/naming.js +10 -0
  260. package/dist/tour/root/tour-root.d.ts +67 -3
  261. package/dist/tour/root/tour-root.js +105 -17
  262. package/dist/tour/stop/tour-stop.d.ts +65 -3
  263. package/dist/tour/stop/tour-stop.js +61 -11
  264. package/dist/tour/wait.d.ts +11 -0
  265. package/dist/tour/wait.js +19 -0
  266. package/dist/tree/item/tree-item.d.ts +21 -5
  267. package/dist/tree/item/tree-item.js +14 -4
  268. package/dist/tree/root/tree-root.d.ts +29 -5
  269. package/dist/tree/root/tree-root.js +64 -8
  270. package/dist/trend/root/trend-root.d.ts +36 -1
  271. package/dist/trend/root/trend-root.js +41 -15
  272. package/dist/video/root/keys.d.ts +5 -0
  273. package/dist/video/root/keys.js +13 -1
  274. package/dist/video/root/video-root.d.ts +19 -0
  275. package/dist/video/root/video-root.js +48 -4
  276. package/dist/video-playlist/menu/video-playlist-menu.d.ts +20 -2
  277. package/dist/video-playlist/menu/video-playlist-menu.js +27 -5
  278. package/dist/video-playlist/root/video-playlist-root.js +6 -3
  279. package/dist/virtual-list/root/virtual-list-root.d.ts +52 -4
  280. package/dist/virtual-list/root/virtual-list-root.js +103 -21
  281. package/lint.json +1 -1
  282. package/package.json +5 -5
  283. package/types/jsx.d.ts +9140 -1149
  284. package/types/svelte.d.ts +1 -0
  285. package/types/vue.d.ts +2089 -267
@@ -4,28 +4,45 @@ 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, DisabledCascadeController, HelixFormElement, hostAria, IndicatorController, KeyboardNavigationController, MixinControllerValue, MixinField, } from '@fusion.dev/helix-core';
7
+ import { commaList, customElement, DisabledCascadeController, HelixFormElement, hostAria, IndicatorController, KeyboardNavigationController, MixinControllerValue, MixinField, 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';
11
11
  let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.lastFocused = undefined;
15
+ this.noteFocus = (event) => {
16
+ const item = this.items.find((one) => one === event.target);
17
+ if (this.multiple && item && item !== this.lastFocused) {
18
+ this.lastFocused = item;
19
+ this.sync();
20
+ }
21
+ };
22
+ this.focusable = true;
14
23
  this.indicator = new IndicatorController(this, {
15
- active: () => this.items.find((item) => item.value === this.value),
24
+ active: () => this.multiple ? undefined : this.items.find((item) => item.value === this.value),
16
25
  indicator: () => this.partNamed('indicator'),
17
26
  observe: () => this.items,
18
27
  surface: () => this.partNamed('track'),
19
28
  });
20
- this.keys = new KeyboardNavigationController(this, { items: () => this.items });
29
+ this.keys = new KeyboardNavigationController(this, {
30
+ items: () => this.items,
31
+ selects: () => !this.multiple,
32
+ });
21
33
  this.cascade = new DisabledCascadeController(this, {
22
34
  items: () => this.items,
23
35
  });
24
36
  this.handleSelection = (event) => {
25
- if (event.defaultPrevented) {
26
- return;
27
- }
28
- this.requestValue(event.detail.value);
37
+ queueMicrotask(() => {
38
+ if (event.defaultPrevented) {
39
+ return;
40
+ }
41
+ const value = event.detail.value;
42
+ this.requestValue(this.multiple
43
+ ? toggleValue(this.values, value, this.items.map((item) => item.value)).join(',')
44
+ : value);
45
+ });
29
46
  };
30
47
  }
31
48
  adopt() {
@@ -38,23 +55,59 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
38
55
  }
39
56
  }
40
57
  sync() {
41
- const active = this.items.find((item) => item.value === this.value);
42
- const entry = active ? undefined : this.items.find((item) => !item.disabled);
58
+ const active = this.multiple ? undefined : this.items.find((item) => item.value === this.value);
59
+ const pressed = new Set(this.values);
60
+ const entry = this.focusable ? this.entry() : undefined;
43
61
  for (const item of this.items) {
44
62
  item.selected = item === active;
63
+ item.pressed = this.multiple ? pressed.has(item.value) : undefined;
45
64
  item.focusable = item === entry;
46
65
  }
47
66
  }
67
+ entry() {
68
+ const items = this.items;
69
+ if (this.multiple) {
70
+ return rovingStop(items, this.lastFocused, {
71
+ disabled: (item) => item.disabled,
72
+ selected: (item) => item.pressed === true,
73
+ });
74
+ }
75
+ const active = items.find((item) => item.value === this.value);
76
+ return active && !active.disabled ? active : items.find((item) => !item.disabled);
77
+ }
78
+ get multiple() {
79
+ return this.selectionMode === 'multiple';
80
+ }
81
+ get focusTarget() {
82
+ return (super.focusTarget ??
83
+ this.entry()?.shadowRoot?.querySelector('[part="item"]') ??
84
+ null);
85
+ }
48
86
  get items() {
49
- return [...this.children].filter((child) => child instanceof HelixHeadlessToggle);
87
+ return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessToggle);
88
+ }
89
+ get answered() {
90
+ if (this.value === '') {
91
+ return false;
92
+ }
93
+ if (this.multiple) {
94
+ return this.children === undefined || this.values.length > 0;
95
+ }
96
+ if (this.children === undefined) {
97
+ return true;
98
+ }
99
+ const items = this.items;
100
+ return items.length === 0 || items.some((item) => item.value === this.value);
50
101
  }
51
102
  connectedCallback() {
52
103
  super.connectedCallback();
53
104
  this.addEventListener('helix-select', this.handleSelection);
105
+ this.addEventListener('focusin', this.noteFocus);
54
106
  }
55
107
  disconnectedCallback() {
56
108
  super.disconnectedCallback();
57
109
  this.removeEventListener('helix-select', this.handleSelection);
110
+ this.removeEventListener('focusin', this.noteFocus);
58
111
  }
59
112
  firstUpdated(changed) {
60
113
  super.firstUpdated(changed);
@@ -70,11 +123,31 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
70
123
  willUpdate(changed) {
71
124
  super.willUpdate(changed);
72
125
  this.adopt();
73
- hostAria(this.internals, { label: this.label, role: 'radiogroup' });
74
- this.internals.setFormValue(this.value === '' ? null : this.value);
126
+ hostAria(this.internals, { label: this.label, role: this.multiple ? 'toolbar' : 'radiogroup' });
127
+ this.internals.ariaOrientation = this.orientation ?? null;
128
+ this.internals.setFormValue(this.formValue());
129
+ }
130
+ formValue() {
131
+ if (!this.answered) {
132
+ return null;
133
+ }
134
+ if (!this.multiple || this.name === '') {
135
+ return this.value;
136
+ }
137
+ const data = new FormData();
138
+ for (const value of this.values) {
139
+ data.append(this.name, value);
140
+ }
141
+ return data;
75
142
  }
76
143
  validate() {
77
- this.reportRequired(this.value === '', 'Choose one to continue.', 'track');
144
+ this.reportRequired(!this.answered, 'Choose one to continue.', 'track');
145
+ }
146
+ get values() {
147
+ return this.multiple ? commaList(this.value) : this.value === '' ? [] : [this.value];
148
+ }
149
+ set values(next) {
150
+ this.value = next.join(',');
78
151
  }
79
152
  render() {
80
153
  const inside = this.labelPlacement === 'start';
@@ -90,6 +163,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
90
163
  this.adopt();
91
164
  this.sync();
92
165
  this.indicator.place();
166
+ this.requestUpdate();
93
167
  }}
94
168
  ></slot>
95
169
  </div>
@@ -98,9 +172,21 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
98
172
  ${this.renderHint()}${this.renderFeedback()}`;
99
173
  }
100
174
  };
175
+ __decorate([
176
+ property({ attribute: false })
177
+ ], HelixHeadlessToggleGroup.prototype, "focusable", void 0);
101
178
  __decorate([
102
179
  property({ attribute: 'label-placement', reflect: true })
103
180
  ], HelixHeadlessToggleGroup.prototype, "labelPlacement", void 0);
181
+ __decorate([
182
+ property({ reflect: true })
183
+ ], HelixHeadlessToggleGroup.prototype, "orientation", void 0);
184
+ __decorate([
185
+ property({ attribute: 'selection-mode', reflect: true })
186
+ ], HelixHeadlessToggleGroup.prototype, "selectionMode", void 0);
187
+ __decorate([
188
+ property({ attribute: false })
189
+ ], HelixHeadlessToggleGroup.prototype, "values", null);
104
190
  HelixHeadlessToggleGroup = __decorate([
105
191
  customElement('helix-headless-toggle-group')
106
192
  ], HelixHeadlessToggleGroup);
@@ -1,11 +1,13 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ 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
5
  * @summary One option in a toggle group.
6
6
  *
7
7
  * @description
8
- * Renders a `<button role="radio">` for use as a form control.
8
+ * Renders a `<button role="radio">` for use as a form control. In a group with
9
+ * `selection-mode="multiple"` it is a `<button>` that says whether it is
10
+ * pressed with `aria-pressed`.
9
11
  *
10
12
  * Do not use it for navigation. For navigation, use links marked with
11
13
  * `aria-current` and styled by the `data-navigation-item` recipe in the base
@@ -23,9 +25,12 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
23
25
  *
24
26
  * @csspart item - The radio button.
25
27
  *
28
+ * @cssstate pressed - The item is pressed, in a group where several can be.
26
29
  * @cssstate selected - The item is chosen.
27
30
  */
28
31
  export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
32
+ /** A click on the element itself, `click()` included, chooses it, as on `Radio`. */
33
+ protected readonly clicks: HostClickController;
29
34
  /**
30
35
  * Whether the markup declares the item chosen. A form reset returns to it.
31
36
  *
@@ -56,23 +61,33 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
56
61
  get defaultSelected(): boolean;
57
62
  set defaultSelected(next: boolean);
58
63
  /**
59
- * Whether the item holds the group's tab stop while nothing is chosen.
64
+ * Whether the item holds the group's tab stop. Unset, the chosen item holds
65
+ * it.
60
66
  *
61
- * The stop normally rides on the selection, and an unanswered group has no
62
- * selection, so without this the keyboard cannot reach it. Written by the
63
- * group, with no attribute: it is coordination between elements, and
64
- * `tabindex` on a host the consumer rendered is the change a React consumer's
65
- * hydration reports and does not patch up.
67
+ * Written by the group on every item: `true` for the chosen item, or for the
68
+ * first item that can be chosen while there is no selection to carry the
69
+ * stop, and `false` for every item, the chosen one too, while a toolbar the
70
+ * group sits in holds the stop. No attribute: it is coordination between
71
+ * elements, and `tabindex` on a host the consumer rendered is the change a
72
+ * React consumer's hydration reports and does not patch up.
66
73
  */
67
- focusable: boolean;
74
+ focusable?: boolean;
75
+ /**
76
+ * Whether the item is pressed, in a group where several can be, and
77
+ * `undefined` in a group where one is chosen.
78
+ *
79
+ * Written by the group, like `selected`, so no attribute.
80
+ */
81
+ pressed?: boolean | undefined;
82
+ protected willUpdate(changed: PropertyValues): void;
68
83
  /**
69
84
  * Asks for the render that follows a hydration, so the stop stops being every
70
85
  * choice's.
71
86
  *
72
87
  * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
73
88
  * false, which is what keeps the hydrating render the server's. Nothing else
74
- * would ask: the group writes an already-false `focusable` onto every choice
75
- * but one, which changes nothing and so re-renders nothing.
89
+ * is sure to ask: a write from the group that leaves `focusable` as it was
90
+ * re-renders nothing.
76
91
  */
77
92
  protected firstUpdated(changed: PropertyValues): void;
78
93
  protected render(): TemplateResult;
@@ -4,13 +4,13 @@ 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, HelixElement, MixinSelectableItem } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
7
+ import { customElement, HelixElement, HostClickController, MixinSelectableItem, } from '@fusion.dev/helix-core';
8
+ import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(HelixElement) {
11
11
  constructor() {
12
12
  super(...arguments);
13
- this.focusable = false;
13
+ this.clicks = new HostClickController(this, () => this.partNamed('item'));
14
14
  }
15
15
  get defaultSelected() {
16
16
  return this.hasAttribute('selected');
@@ -18,6 +18,10 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
18
18
  set defaultSelected(next) {
19
19
  this.toggleAttribute('selected', next);
20
20
  }
21
+ willUpdate(changed) {
22
+ super.willUpdate(changed);
23
+ this.toggleState('pressed', this.pressed === true);
24
+ }
21
25
  firstUpdated(changed) {
22
26
  super.firstUpdated(changed);
23
27
  this.finishHydration();
@@ -26,10 +30,11 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
26
30
  return html `<button
27
31
  part="item"
28
32
  type="button"
29
- role="radio"
30
- aria-checked=${this.selected ? 'true' : 'false'}
33
+ role=${this.pressed === undefined ? 'radio' : nothing}
34
+ aria-checked=${this.pressed === undefined ? (this.selected ? 'true' : 'false') : nothing}
35
+ aria-pressed=${this.pressed === undefined ? nothing : String(this.pressed)}
31
36
  ?disabled=${this.disabled}
32
- tabindex=${this.selected || this.focusable || !this.childrenKnown ? '0' : '-1'}
37
+ tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
33
38
  @click=${() => this.select()}
34
39
  >
35
40
  <slot></slot>
@@ -39,6 +44,9 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
39
44
  __decorate([
40
45
  property({ attribute: false })
41
46
  ], HelixHeadlessToggle.prototype, "focusable", void 0);
47
+ __decorate([
48
+ property({ attribute: false })
49
+ ], HelixHeadlessToggle.prototype, "pressed", void 0);
42
50
  HelixHeadlessToggle = __decorate([
43
51
  customElement('helix-headless-toggle')
44
52
  ], HelixHeadlessToggle);
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A group of controls that share one tab stop.
@@ -9,7 +9,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
9
9
  *
10
10
  * The toolbar is one tab stop, and the arrow keys move between its controls, so
11
11
  * three toolbars take three presses of Tab to pass. Any element the browser can
12
- * focus counts as a control, including your own elements.
12
+ * focus counts as a control, including your own elements. A child that takes no
13
+ * focus, such as a divider, is skipped. A menu is reached at its trigger.
14
+ *
15
+ * A nested composite, such as a toggle group, is one stop on the toolbar's
16
+ * axis: the toolbar's arrows move past it, and the arrows across the toolbar
17
+ * move inside it. A composite with a `focusable` property, as Toggle Group
18
+ * has, gives up its own tab stop while the toolbar's is elsewhere, so Tab
19
+ * stops once in the toolbar. A composite without one keeps its own tab stop.
13
20
  *
14
21
  * @since 0.1.0
15
22
  * @status stable
@@ -24,27 +31,50 @@ import { type PropertyValues, type TemplateResult } from 'lit';
24
31
  export declare class HelixHeadlessToolbar extends HelixElement {
25
32
  /**
26
33
  * The toolbar's name, read by screen readers.
34
+ *
35
+ * An `aria-label` or `aria-labelledby` on the host names the toolbar too,
36
+ * and wins over this.
27
37
  */
28
38
  label: string;
39
+ /** Carries a host `aria-label` and `aria-labelledby` to the toolbar. */
40
+ protected readonly aria: AriaForwardController;
41
+ /**
42
+ * Whether the stop has been written onto controls this package does not
43
+ * know, which happens on the first focus. From then on, every move of the
44
+ * stop has to write them too, or the one at `0` can be removed or disabled
45
+ * and leave every other one at `-1`.
46
+ */
47
+ private wroteForeign;
29
48
  /**
30
49
  * The direction the arrow keys move focus in.
31
50
  */
32
51
  orientation?: 'horizontal' | 'vertical';
33
52
  private watcher;
34
53
  /**
35
- * Children that can take the tab stop, in document order.
36
- *
37
- * Every child that is not `disabled` and not `aria-hidden`, which is
38
- * deliberately a test on the element rather than a list of tag names: a
39
- * toolbar holds anything, including elements this package has never heard of.
40
- * Client only, there being no tree to walk on the server.
54
+ * The children that take part in the toolbar, in document order, each with
55
+ * the elements in it that take focus.
41
56
  *
42
- * **It does not ask a child whether it is focusable.** Reading a `focusable`
43
- * property is what `setTabStop` does when moving the stop, not what this does
44
- * when choosing the set, so an element that is neither disabled nor hidden and
45
- * still cannot take focus is counted here and skipped there.
57
+ * A test on the element rather than a list of tag names: a toolbar holds
58
+ * anything, including elements this package has never heard of. A child
59
+ * that takes no focus and holds nothing that does is skipped. Client only,
60
+ * there being no tree to walk on the server.
46
61
  */
47
62
  private get controls();
63
+ /** The elements in a child's light DOM that take focus. */
64
+ private within;
65
+ /**
66
+ * The stops of a child that takes focus itself: the child, or the members of
67
+ * a composite that owns its tab stop, such as a toggle group, which the
68
+ * toolbar enters at a member and moves the stop of through `focusable`.
69
+ */
70
+ private inside;
71
+ /**
72
+ * Where focus lands on arriving at a control. A nested composite is entered
73
+ * at the stop it holds, which for a toggle group is the chosen toggle.
74
+ */
75
+ private entry;
76
+ /** The control a press or a focus came from, by its path. */
77
+ private origin;
48
78
  /**
49
79
  * Moves the single tab stop to one control.
50
80
  *
@@ -74,9 +104,10 @@ export declare class HelixHeadlessToolbar extends HelixElement {
74
104
  * already at `-1`, leaving a toolbar the keyboard cannot reach at all. A Save
75
105
  * button disabling itself once the form is clean is enough to do it.
76
106
  *
77
- * `foreign` stays false, the same as the first claim: a control this package
78
- * has never heard of that was never given `tabindex="-1"` is still in the
79
- * document's own tab order, so it is never the one that gets stranded.
107
+ * `foreign` stays false, the same as the first claim, until a focus has
108
+ * written `tabindex="-1"` onto foreign controls. From then on `setTabStop`
109
+ * writes them as well, so removing or disabling the native button that held
110
+ * the stop hands it to the first one left.
80
111
  */
81
112
  private readonly restoreTabStop;
82
113
  /**
@@ -86,6 +117,15 @@ export declare class HelixHeadlessToolbar extends HelixElement {
86
117
  * so a reflected default cannot appear on the host after hydration.
87
118
  */
88
119
  private get axis();
120
+ /**
121
+ * The toolbar's own keys, from inside a nested composite.
122
+ *
123
+ * In capture, ahead of the composite, which answers every arrow itself and
124
+ * would otherwise hold the reader inside it. Only the keys along the
125
+ * toolbar's axis and Home and End are taken, so the arrows across it still
126
+ * move inside the composite.
127
+ */
128
+ private readonly handleCompositeKeydown;
89
129
  private readonly handleKeydown;
90
130
  connectedCallback(): void;
91
131
  disconnectedCallback(): void;
@@ -4,28 +4,63 @@ 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, HelixElement, inlineStep, keyClaimed } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixElement, inlineStep, keyClaimed, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  const KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
11
11
  const ownsTabStop = (node) => 'focusable' in node;
12
+ const NATIVE = 'a[href], button, input, select, textarea, [tabindex], [contenteditable=""], [contenteditable="true"]';
13
+ const TABBABLE = NATIVE.replace('[tabindex]', '[tabindex]:not([tabindex="-1"])');
14
+ const takesFocus = (node) => {
15
+ if (!(node instanceof HTMLElement) ||
16
+ node.hasAttribute('disabled') ||
17
+ node.getAttribute('aria-hidden') === 'true') {
18
+ return false;
19
+ }
20
+ return (node.matches(NATIVE) ||
21
+ ownsTabStop(node) ||
22
+ node.shadowRoot?.delegatesFocus === true ||
23
+ node.shadowRoot?.querySelector(TABBABLE) != null);
24
+ };
12
25
  let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
13
26
  constructor() {
14
27
  super(...arguments);
15
28
  this.label = 'Toolbar';
29
+ this.aria = new AriaForwardController(this, {
30
+ label: true,
31
+ target: () => this.partNamed('toolbar'),
32
+ });
33
+ this.wroteForeign = false;
16
34
  this.handleFocusIn = (event) => {
17
- const control = this.controls.find((entry) => entry.contains(event.target));
35
+ const control = this.origin(event);
18
36
  if (control) {
19
37
  this.setTabStop(control);
20
38
  }
21
39
  };
22
40
  this.restoreTabStop = () => {
23
41
  const controls = this.controls;
24
- const held = controls.some((control) => ownsTabStop(control) ? control.focusable : control.tabIndex === 0);
42
+ const held = controls.some((control) => {
43
+ const composite = control.stops.length !== 1;
44
+ const element = composite ? control.child : control.stops[0];
45
+ if (ownsTabStop(element)) {
46
+ return element.focusable;
47
+ }
48
+ return composite || element.tabIndex === 0;
49
+ });
25
50
  if (controls.length > 0 && !held) {
26
51
  this.setTabStop(controls[0], false);
27
52
  }
28
53
  };
54
+ this.handleCompositeKeydown = (event) => {
55
+ const control = this.origin(event);
56
+ if (!control || control.stops.length === 1) {
57
+ return;
58
+ }
59
+ this.handleKeydown(event);
60
+ if (event.defaultPrevented) {
61
+ event.stopPropagation();
62
+ }
63
+ };
29
64
  this.handleKeydown = (event) => {
30
65
  if (!KEYS.has(event.key) || keyClaimed(event)) {
31
66
  return;
@@ -34,7 +69,8 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
34
69
  const forward = this.axis === 'horizontal' ? along === 1 : event.key === 'ArrowDown';
35
70
  const back = this.axis === 'horizontal' ? along === -1 : event.key === 'ArrowUp';
36
71
  const controls = this.controls;
37
- const current = controls.findIndex((entry) => entry.contains(event.target));
72
+ const origin = this.origin(event);
73
+ const current = origin ? controls.findIndex((control) => control.child === origin.child) : -1;
38
74
  if (current === -1) {
39
75
  return;
40
76
  }
@@ -57,30 +93,75 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
57
93
  event.preventDefault();
58
94
  const target = controls[next];
59
95
  this.setTabStop(target);
60
- target?.focus();
96
+ const element = target ? this.entry(target) : undefined;
97
+ if (element && 'focusItem' in element && typeof element.focusItem === 'function') {
98
+ element.focusItem();
99
+ }
100
+ else {
101
+ element?.focus();
102
+ }
61
103
  };
62
104
  }
63
105
  get controls() {
64
- return [...this.children].filter((child) => {
65
- if (!(child instanceof HTMLElement)) {
66
- return false;
106
+ const controls = [];
107
+ for (const child of this.children) {
108
+ if (!(child instanceof HTMLElement) ||
109
+ child.hasAttribute('disabled') ||
110
+ child.getAttribute('aria-hidden') === 'true') {
111
+ continue;
67
112
  }
68
- return !child.hasAttribute('disabled') && child.getAttribute('aria-hidden') !== 'true';
69
- });
113
+ const stops = takesFocus(child) ? this.inside(child) : this.within(child);
114
+ if (stops.length > 0) {
115
+ controls.push({ child, stops });
116
+ }
117
+ }
118
+ return controls;
119
+ }
120
+ within(child) {
121
+ return [...child.querySelectorAll('*')].filter(takesFocus);
122
+ }
123
+ inside(child) {
124
+ if (!ownsTabStop(child)) {
125
+ return [child];
126
+ }
127
+ const members = this.within(child);
128
+ return members.length > 1 ? members : [child];
129
+ }
130
+ entry(control) {
131
+ const { stops } = control;
132
+ if (stops.length === 1) {
133
+ return stops[0];
134
+ }
135
+ return (stops.find((stop) => (ownsTabStop(stop) && stop.focusable) ||
136
+ stop.selected === true) ?? stops[0]);
137
+ }
138
+ origin(event) {
139
+ const path = event.composedPath();
140
+ return this.controls.find((control) => control.stops.some((stop) => path.includes(stop)));
70
141
  }
71
142
  setTabStop(active, foreign = true) {
143
+ if (foreign) {
144
+ this.wroteForeign = true;
145
+ }
72
146
  for (const control of this.controls) {
73
- const stop = control === active;
74
- if (ownsTabStop(control)) {
75
- control.focusable = stop;
147
+ if (control.stops.length !== 1) {
148
+ if (ownsTabStop(control.child)) {
149
+ control.child.focusable = control.child === active?.child;
150
+ }
151
+ continue;
152
+ }
153
+ const element = control.stops[0];
154
+ const stop = control.child === active?.child;
155
+ if (ownsTabStop(element)) {
156
+ element.focusable = stop;
76
157
  }
77
- else if (foreign) {
78
- control.tabIndex = stop ? 0 : -1;
158
+ else if (foreign || this.wroteForeign) {
159
+ element.tabIndex = stop ? 0 : -1;
79
160
  }
80
161
  }
81
162
  }
82
163
  async claimTabStop() {
83
- const pending = [...new Set(this.controls.map((control) => control.localName))].filter((name) => name.includes('-'));
164
+ const pending = [...new Set([...this.children].map((child) => child.localName))].filter((name) => name.includes('-'));
84
165
  await Promise.all(pending.map((name) => customElements.whenDefined(name)));
85
166
  this.setTabStop(this.controls[0], false);
86
167
  }
@@ -89,6 +170,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
89
170
  }
90
171
  connectedCallback() {
91
172
  super.connectedCallback();
173
+ this.addEventListener('keydown', this.handleCompositeKeydown, true);
92
174
  this.addEventListener('keydown', this.handleKeydown);
93
175
  this.addEventListener('focusin', this.handleFocusIn);
94
176
  this.watcher = new MutationObserver(this.restoreTabStop);
@@ -100,6 +182,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
100
182
  }
101
183
  disconnectedCallback() {
102
184
  super.disconnectedCallback();
185
+ this.removeEventListener('keydown', this.handleCompositeKeydown, true);
103
186
  this.removeEventListener('keydown', this.handleKeydown);
104
187
  this.removeEventListener('focusin', this.handleFocusIn);
105
188
  this.watcher?.disconnect();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The element focus lands on when a trigger is focused: the trigger itself,
3
+ * or, for a component that delegates focus, the control inside its shadow root,
4
+ * followed down through any component nested there.
5
+ */
6
+ export declare const focusTarget: (trigger: Element) => Element;
@@ -0,0 +1,9 @@
1
+ const CONTROL = 'a[href], button, input, select, textarea, [contenteditable], [tabindex]:not([tabindex="-1"])';
2
+ export const focusTarget = (trigger) => {
3
+ const root = trigger.shadowRoot;
4
+ if (!root?.delegatesFocus) {
5
+ return trigger;
6
+ }
7
+ const inner = root.querySelector(CONTROL);
8
+ return inner ? focusTarget(inner) : trigger;
9
+ };