@fusion.dev/helix-headless 0.143.0 → 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 (191) hide show
  1. package/custom-elements.json +8551 -375
  2. package/dist/area-chart/root/area-chart-root.d.ts +12 -4
  3. package/dist/area-chart/root/area-chart-root.js +24 -7
  4. package/dist/avatar/root/avatar-root.d.ts +14 -0
  5. package/dist/avatar/root/avatar-root.js +23 -9
  6. package/dist/badge/root/badge-root.d.ts +11 -7
  7. package/dist/badge/root/badge-root.js +1 -1
  8. package/dist/bar-chart/root/bar-chart-root.d.ts +12 -3
  9. package/dist/bar-chart/root/bar-chart-root.js +17 -10
  10. package/dist/button/root/button-root.d.ts +47 -1
  11. package/dist/button/root/button-root.js +38 -4
  12. package/dist/callout/root/callout-root.d.ts +18 -1
  13. package/dist/callout/root/callout-root.js +15 -4
  14. package/dist/card/root/card-root.d.ts +30 -1
  15. package/dist/card/root/card-root.js +38 -5
  16. package/dist/checkbox/root/checkbox-root.js +2 -0
  17. package/dist/checkbox-group/root/checkbox-group-root.d.ts +53 -3
  18. package/dist/checkbox-group/root/checkbox-group-root.js +71 -22
  19. package/dist/code-block/lines.d.ts +13 -0
  20. package/dist/code-block/lines.js +2 -0
  21. package/dist/code-block/root/code-block-root.d.ts +30 -0
  22. package/dist/code-block/root/code-block-root.js +23 -2
  23. package/dist/color-input/root/color-input-root.d.ts +34 -1
  24. package/dist/color-input/root/color-input-root.js +23 -4
  25. package/dist/color-picker/channels.d.ts +26 -0
  26. package/dist/color-picker/channels.js +84 -0
  27. package/dist/color-picker/root/color-picker-root.d.ts +81 -5
  28. package/dist/color-picker/root/color-picker-root.js +103 -10
  29. package/dist/combobox/root/combobox-root.d.ts +38 -3
  30. package/dist/combobox/root/combobox-root.js +64 -2
  31. package/dist/command/root/command-root.d.ts +30 -4
  32. package/dist/command/root/command-root.js +33 -3
  33. package/dist/copy-button/from.d.ts +22 -0
  34. package/dist/copy-button/from.js +28 -0
  35. package/dist/copy-button/root/copy-button-root.d.ts +27 -1
  36. package/dist/copy-button/root/copy-button-root.js +43 -7
  37. package/dist/date-input/root/date-input-root.d.ts +73 -4
  38. package/dist/date-input/root/date-input-root.js +93 -43
  39. package/dist/date-picker/root/date-picker-root.d.ts +145 -1
  40. package/dist/date-picker/root/date-picker-root.js +239 -48
  41. package/dist/dialog/description.d.ts +9 -0
  42. package/dist/dialog/description.js +14 -0
  43. package/dist/dialog/root/dialog-root.d.ts +64 -1
  44. package/dist/dialog/root/dialog-root.js +49 -23
  45. package/dist/donut-chart/root/donut-chart-root.d.ts +25 -4
  46. package/dist/donut-chart/root/donut-chart-root.js +21 -6
  47. package/dist/drawer/root/drawer-root.d.ts +63 -2
  48. package/dist/drawer/root/drawer-root.js +95 -19
  49. package/dist/drawer/swipe-start.d.ts +9 -0
  50. package/dist/drawer/swipe-start.js +12 -0
  51. package/dist/element-events.generated.js +8 -5
  52. package/dist/empty/root/empty-root.d.ts +1 -1
  53. package/dist/file-input/root/file-input-root.d.ts +47 -0
  54. package/dist/file-input/root/file-input-root.js +60 -22
  55. package/dist/file-input/screen.d.ts +35 -0
  56. package/dist/file-input/screen.js +49 -0
  57. package/dist/filter-input/root/filter-input-root.d.ts +38 -4
  58. package/dist/filter-input/root/filter-input-root.js +67 -3
  59. package/dist/filter-input/suggest.d.ts +17 -0
  60. package/dist/filter-input/suggest.js +23 -0
  61. package/dist/format-date/options.d.ts +33 -0
  62. package/dist/format-date/options.js +42 -0
  63. package/dist/format-date/root/format-date-root.d.ts +51 -0
  64. package/dist/format-date/root/format-date-root.js +58 -38
  65. package/dist/heatmap/root/heatmap-root.d.ts +23 -6
  66. package/dist/heatmap/root/heatmap-root.js +24 -13
  67. package/dist/icon/index.d.ts +2 -0
  68. package/dist/icon/index.js +2 -0
  69. package/dist/icon/registry.d.ts +37 -0
  70. package/dist/icon/registry.js +83 -0
  71. package/dist/icon/root/icon-root.d.ts +30 -4
  72. package/dist/icon/root/icon-root.js +41 -6
  73. package/dist/icon/root/index.d.ts +2 -0
  74. package/dist/icon/root/index.js +2 -0
  75. package/dist/icon/set.d.ts +1 -0
  76. package/dist/icon/set.js +3 -0
  77. package/dist/image/root/image-root.d.ts +14 -0
  78. package/dist/image/root/image-root.js +15 -2
  79. package/dist/input/root/input-root.d.ts +11 -0
  80. package/dist/input/root/input-root.js +15 -0
  81. package/dist/kbd/root/kbd-root.d.ts +20 -13
  82. package/dist/kbd/root/kbd-root.js +35 -37
  83. package/dist/kbd/shortcut.d.ts +18 -0
  84. package/dist/kbd/shortcut.js +35 -0
  85. package/dist/legend/hidden-series.d.ts +8 -0
  86. package/dist/legend/hidden-series.js +13 -0
  87. package/dist/legend/root/legend-root.d.ts +56 -0
  88. package/dist/legend/root/legend-root.js +54 -5
  89. package/dist/lightbox/item/lightbox-item.d.ts +15 -0
  90. package/dist/lightbox/item/lightbox-item.js +10 -1
  91. package/dist/lightbox/root/lightbox-root.d.ts +61 -1
  92. package/dist/lightbox/root/lightbox-root.js +94 -4
  93. package/dist/line-chart/root/line-chart-root.d.ts +11 -3
  94. package/dist/line-chart/root/line-chart-root.js +19 -5
  95. package/dist/listbox/range.d.ts +17 -0
  96. package/dist/listbox/range.js +5 -0
  97. package/dist/listbox/root/listbox-root.d.ts +74 -3
  98. package/dist/listbox/root/listbox-root.js +87 -9
  99. package/dist/loader/busy.d.ts +17 -0
  100. package/dist/loader/busy.js +24 -0
  101. package/dist/loader/root/loader-root.d.ts +37 -0
  102. package/dist/loader/root/loader-root.js +52 -3
  103. package/dist/meter/root/meter-root.d.ts +7 -0
  104. package/dist/meter/root/meter-root.js +6 -3
  105. package/dist/notifications/feed-keys.d.ts +10 -0
  106. package/dist/notifications/feed-keys.js +14 -0
  107. package/dist/notifications/notification/notification.d.ts +17 -0
  108. package/dist/notifications/notification/notification.js +21 -2
  109. package/dist/notifications/root/notifications-root.d.ts +42 -0
  110. package/dist/notifications/root/notifications-root.js +60 -1
  111. package/dist/number-input/root/number-input-root.d.ts +47 -0
  112. package/dist/number-input/root/number-input-root.js +58 -22
  113. package/dist/object-inspector/root/object-inspector-root.d.ts +1 -0
  114. package/dist/object-inspector/root/object-inspector-root.js +1 -1
  115. package/dist/option/root/option-root.d.ts +54 -5
  116. package/dist/option/root/option-root.js +34 -5
  117. package/dist/pin-input/format.d.ts +7 -0
  118. package/dist/pin-input/format.js +16 -0
  119. package/dist/pin-input/root/pin-input-root.d.ts +21 -0
  120. package/dist/pin-input/root/pin-input-root.js +29 -2
  121. package/dist/popover/root/popover-root.d.ts +62 -4
  122. package/dist/popover/root/popover-root.js +53 -5
  123. package/dist/progress/root/progress-root.d.ts +7 -0
  124. package/dist/progress/root/progress-root.js +9 -3
  125. package/dist/qr-code/export.d.ts +31 -0
  126. package/dist/qr-code/export.js +10 -0
  127. package/dist/qr-code/root/qr-code-root.d.ts +26 -0
  128. package/dist/qr-code/root/qr-code-root.js +49 -2
  129. package/dist/radio/group/radio-group.d.ts +23 -0
  130. package/dist/radio/group/radio-group.js +18 -0
  131. package/dist/rating/root/rating-root.d.ts +25 -0
  132. package/dist/rating/root/rating-root.js +45 -3
  133. package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
  134. package/dist/scatter-chart/root/scatter-chart-root.js +25 -7
  135. package/dist/select/root/select-root.d.ts +24 -0
  136. package/dist/select/root/select-root.js +20 -6
  137. package/dist/skeleton/root/skeleton-root.d.ts +25 -0
  138. package/dist/skeleton/root/skeleton-root.js +17 -2
  139. package/dist/slider/root/slider-root.d.ts +39 -0
  140. package/dist/slider/root/slider-root.js +45 -9
  141. package/dist/stat/root/stat-root.d.ts +2 -0
  142. package/dist/stat/root/stat-root.js +1 -1
  143. package/dist/steps/step/step.d.ts +1 -0
  144. package/dist/steps/step/step.js +1 -1
  145. package/dist/switch/root/switch-root.d.ts +12 -2
  146. package/dist/switch/root/switch-root.js +6 -1
  147. package/dist/tag/group/tag-group.d.ts +107 -0
  148. package/dist/tag/group/tag-group.js +157 -0
  149. package/dist/tag/index.d.ts +1 -0
  150. package/dist/tag/index.js +1 -0
  151. package/dist/tag/root/tag-root.d.ts +60 -0
  152. package/dist/tag/root/tag-root.js +74 -18
  153. package/dist/tag/selection.d.ts +10 -0
  154. package/dist/tag/selection.js +12 -0
  155. package/dist/textarea/grow.d.ts +22 -0
  156. package/dist/textarea/grow.js +20 -0
  157. package/dist/textarea/root/textarea-root.d.ts +9 -0
  158. package/dist/textarea/root/textarea-root.js +9 -1
  159. package/dist/time-input/root/time-input-root.d.ts +44 -5
  160. package/dist/time-input/root/time-input-root.js +69 -39
  161. package/dist/time-picker/root/time-picker-root.d.ts +54 -8
  162. package/dist/time-picker/root/time-picker-root.js +121 -91
  163. package/dist/toast/hotkey.d.ts +22 -0
  164. package/dist/toast/hotkey.js +23 -0
  165. package/dist/toast/item/toast-item.d.ts +16 -1
  166. package/dist/toast/item/toast-item.js +41 -13
  167. package/dist/toast/root/toast-root.d.ts +42 -8
  168. package/dist/toast/root/toast-root.js +64 -4
  169. package/dist/toggle/group/toggle-group.d.ts +57 -15
  170. package/dist/toggle/group/toggle-group.js +69 -9
  171. package/dist/toggle/root/toggle-root.d.ts +12 -1
  172. package/dist/toggle/root/toggle-root.js +11 -3
  173. package/dist/tooltip/focus-target.d.ts +6 -0
  174. package/dist/tooltip/focus-target.js +9 -0
  175. package/dist/tooltip/root/tooltip-root.d.ts +42 -17
  176. package/dist/tooltip/root/tooltip-root.js +50 -9
  177. package/dist/tour/naming.d.ts +30 -0
  178. package/dist/tour/naming.js +10 -0
  179. package/dist/tour/root/tour-root.d.ts +35 -0
  180. package/dist/tour/root/tour-root.js +56 -5
  181. package/dist/tour/stop/tour-stop.d.ts +53 -8
  182. package/dist/tour/stop/tour-stop.js +45 -15
  183. package/dist/tour/wait.d.ts +11 -0
  184. package/dist/tour/wait.js +19 -0
  185. package/dist/trend/root/trend-root.d.ts +13 -0
  186. package/dist/trend/root/trend-root.js +14 -9
  187. package/lint.json +1 -1
  188. package/package.json +5 -5
  189. package/types/jsx.d.ts +4862 -254
  190. package/types/svelte.d.ts +1 -0
  191. package/types/vue.d.ts +1149 -67
@@ -4,13 +4,37 @@ 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, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
7
+ import { customElement, deepActiveElement, deepContains, FocusScopeController, fillTemplate, focusableElements, HelixElement, hostAria, scrollHolders, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { matchesHotkey, parseHotkey } from '../hotkey.js';
10
11
  let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
11
12
  constructor() {
12
13
  super(...arguments);
13
- this.label = 'Notifications';
14
+ this.hotkey = 'F8';
15
+ this.label = 'Notifications ({hotkey})';
16
+ this.scope = new FocusScopeController(this, {
17
+ contents: () => this.showing.flatMap((one) => {
18
+ const target = focusTarget(one);
19
+ return target ? [target] : [];
20
+ }),
21
+ surface: () => this.region,
22
+ });
23
+ this.handleHotkey = (event) => {
24
+ if (event.defaultPrevented || !matchesHotkey(event, parseHotkey(this.hotkey))) {
25
+ return;
26
+ }
27
+ if (this.scope.holdsFocus) {
28
+ event.preventDefault();
29
+ this.scope.leave();
30
+ return;
31
+ }
32
+ if (this.showing.length > 0) {
33
+ event.preventDefault();
34
+ this.scope.enter();
35
+ }
36
+ };
37
+ this.made = new WeakSet();
14
38
  this.handleFocusIn = (event) => {
15
39
  const from = event.relatedTarget;
16
40
  if (from instanceof HTMLElement && !deepContains(this, from)) {
@@ -57,11 +81,41 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
57
81
  }
58
82
  willUpdate(changed) {
59
83
  super.willUpdate(changed);
60
- hostAria(this.internals, { label: this.label, role: 'status' });
84
+ hostAria(this.internals, {
85
+ label: fillTemplate(this.label, { hotkey: this.hotkey }),
86
+ role: 'region',
87
+ });
61
88
  this.internals.ariaLive = 'polite';
62
89
  this.internals.ariaRelevant = 'additions';
63
90
  this.internals.ariaAtomic = 'false';
64
91
  }
92
+ add(message, options = {}) {
93
+ const tier = this.constructor.tier;
94
+ const item = document.createElement(`${tier}toast-item`);
95
+ item.textContent = message;
96
+ if (options.tone) {
97
+ item.setAttribute('tone', options.tone);
98
+ }
99
+ if (options.duration !== undefined) {
100
+ item.setAttribute('duration', String(options.duration));
101
+ }
102
+ if (options.dismissible ?? true) {
103
+ item.toggleAttribute('dismissible', true);
104
+ }
105
+ this.made.add(item);
106
+ this.append(item);
107
+ return item;
108
+ }
109
+ sweep() {
110
+ for (const one of [...this.children]) {
111
+ if (!(one instanceof HTMLElement) || !one.hidden || !this.made.has(one)) {
112
+ continue;
113
+ }
114
+ this.made.delete(one);
115
+ const running = typeof one.getAnimations === 'function' ? one.getAnimations({ subtree: true }) : [];
116
+ void Promise.allSettled(running.map((animation) => animation.finished)).then(() => one.remove());
117
+ }
118
+ }
65
119
  get showing() {
66
120
  return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden);
67
121
  }
@@ -129,8 +183,10 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
129
183
  super.connectedCallback();
130
184
  this.addEventListener('focusin', this.handleFocusIn);
131
185
  this.addEventListener('helix-dismiss', this.handleDismiss);
186
+ document.addEventListener('keydown', this.handleHotkey);
132
187
  this.watcher = new MutationObserver((records) => {
133
188
  this.place(records.flatMap((record) => [...record.addedNodes]));
189
+ this.sweep();
134
190
  });
135
191
  this.watcher.observe(this, {
136
192
  attributeFilter: ['hidden'],
@@ -146,6 +202,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
146
202
  super.disconnectedCallback();
147
203
  this.removeEventListener('focusin', this.handleFocusIn);
148
204
  this.removeEventListener('helix-dismiss', this.handleDismiss);
205
+ document.removeEventListener('keydown', this.handleHotkey);
149
206
  this.watcher?.disconnect();
150
207
  this.watcher = undefined;
151
208
  }
@@ -154,9 +211,12 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
154
211
  this.place();
155
212
  }
156
213
  render() {
157
- return html `<div part="region" popover="manual"><slot></slot></div>`;
214
+ return html `<div part="region" popover="manual" tabindex="-1"><slot></slot></div>`;
158
215
  }
159
216
  };
217
+ __decorate([
218
+ property()
219
+ ], HelixHeadlessToast.prototype, "hotkey", void 0);
160
220
  __decorate([
161
221
  property()
162
222
  ], HelixHeadlessToast.prototype, "label", void 0);
@@ -14,6 +14,17 @@ declare const HelixHeadlessToggleGroup_base: (new (...args: any[]) => import("@f
14
14
  * Measurement happens on the client, so the server renders the indicator hidden
15
15
  * and it fades in once measured.
16
16
  *
17
+ * `selection-mode="multiple"` makes the group a toolbar of buttons that each
18
+ * say whether they are pressed with `aria-pressed`, as React Aria's
19
+ * ToggleButtonGroup and Radix's ToggleGroup do. A press presses or releases
20
+ * one item, the arrow keys move focus without pressing, and `value` is the
21
+ * pressed values, comma separated, which `values` also gives as a list. The
22
+ * form receives one entry per pressed value. The indicator stays hidden, and
23
+ * each pressed item is marked with `:state(pressed)`.
24
+ *
25
+ * `orientation="vertical"` runs the items down a column, says so to the
26
+ * accessibility tree, and the up and down arrows move between them.
27
+ *
17
28
  * @since 0.1.0
18
29
  * @status stable
19
30
  * @aliases SegmentedControl, Segmented, Navigation, Switcher
@@ -98,6 +109,11 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
98
109
  * goes to the first item that can be chosen, as in `Radio.Group`.
99
110
  */
100
111
  private entry;
112
+ /** The item focus was last on, in a toolbar, which holds its tab stop. */
113
+ private lastFocused;
114
+ private readonly noteFocus;
115
+ /** Whether several items can be pressed at once. */
116
+ private get multiple();
101
117
  /**
102
118
  * Whether the group holds a tab stop of its own.
103
119
  *
@@ -110,6 +126,37 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
110
126
  * reason to set it.
111
127
  */
112
128
  focusable: boolean;
129
+ /**
130
+ * Where the group's label sits. `start` puts it on the track's line.
131
+ *
132
+ * Absent is above, which is what a field looks like and what almost every
133
+ * group wants. `start` is for a row of controls that are labeled rather than
134
+ * a form of fields: a toolbar reads as one line, and a label stacked over
135
+ * every group in it turns one row into two.
136
+ *
137
+ * **A placement rather than a slot, and that is the whole point of it.** A
138
+ * `start` slot would let a consumer put the word there, and would also let
139
+ * them put it there *instead of* setting `label`, which is the group's
140
+ * accessible name: the set would then be named for nobody using a screen
141
+ * reader while looking correct to everybody else. Worse, setting both draws
142
+ * it twice. One property moves the label the consumer already has to give,
143
+ * and cannot be used to remove it.
144
+ */
145
+ labelPlacement?: 'start';
146
+ /**
147
+ * The direction the items run. `vertical` puts them in a column, and the up
148
+ * and down arrows move between them.
149
+ *
150
+ * Optional, so the attribute is absent until a consumer writes it.
151
+ */
152
+ orientation?: 'horizontal' | 'vertical';
153
+ /**
154
+ * Whether one item is chosen, as a radio group, or several can be pressed,
155
+ * as a toolbar of buttons with `aria-pressed`.
156
+ *
157
+ * Optional, so the attribute is absent until a consumer writes it.
158
+ */
159
+ selectionMode?: 'multiple' | 'single';
113
160
  /**
114
161
  * Focus goes to the item that holds the stop, and to the item that would
115
162
  * hold it while a toolbar holds the stop instead, so a label press or a
@@ -132,6 +179,11 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
132
179
  protected firstUpdated(changed: PropertyValues): void;
133
180
  protected updated(changed: PropertyValues): void;
134
181
  protected willUpdate(changed: PropertyValues): void;
182
+ /**
183
+ * What the form receives: the value, or under `selection-mode="multiple"`
184
+ * one entry per pressed value, as a native `<select multiple>` sends.
185
+ */
186
+ private formValue;
135
187
  /**
136
188
  * The verdict on the answer `required` asks for.
137
189
  *
@@ -145,22 +197,12 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
145
197
  */
146
198
  private validate;
147
199
  /**
148
- * Where the group's label sits. `start` puts it on the track's line.
149
- *
150
- * Absent is above, which is what a field looks like and what almost every
151
- * group wants. `start` is for a row of controls that are labeled rather than
152
- * a form of fields: a toolbar reads as one line, and a label stacked over
153
- * every group in it turns one row into two.
154
- *
155
- * **A placement rather than a slot, and that is the whole point of it.** A
156
- * `start` slot would let a consumer put the word there, and would also let
157
- * them put it there *instead of* setting `label`, which is the group's
158
- * accessible name: the set would then be named for nobody using a screen
159
- * reader while looking correct to everybody else. Worse, setting both draws
160
- * it twice. One property moves the label the consumer already has to give,
161
- * and cannot be used to remove it.
200
+ * The pressed values as a list, under `selection-mode="multiple"`, in the
201
+ * order the items are written. Written, it presses exactly those items.
202
+ * Where one item is chosen, it is that one value, or none.
162
203
  */
163
- labelPlacement?: 'start';
204
+ get values(): string[];
205
+ set values(next: string[]);
164
206
  /**
165
207
  * The row the track sits in, so something can sit beside it.
166
208
  *
@@ -4,21 +4,32 @@ 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
+ };
14
22
  this.focusable = true;
15
23
  this.indicator = new IndicatorController(this, {
16
- active: () => this.items.find((item) => item.value === this.value),
24
+ active: () => this.multiple ? undefined : this.items.find((item) => item.value === this.value),
17
25
  indicator: () => this.partNamed('indicator'),
18
26
  observe: () => this.items,
19
27
  surface: () => this.partNamed('track'),
20
28
  });
21
- this.keys = new KeyboardNavigationController(this, { items: () => this.items });
29
+ this.keys = new KeyboardNavigationController(this, {
30
+ items: () => this.items,
31
+ selects: () => !this.multiple,
32
+ });
22
33
  this.cascade = new DisabledCascadeController(this, {
23
34
  items: () => this.items,
24
35
  });
@@ -27,7 +38,10 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
27
38
  if (event.defaultPrevented) {
28
39
  return;
29
40
  }
30
- this.requestValue(event.detail.value);
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);
31
45
  });
32
46
  };
33
47
  }
@@ -41,16 +55,28 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
41
55
  }
42
56
  }
43
57
  sync() {
44
- const active = this.items.find((item) => item.value === this.value);
58
+ const active = this.multiple ? undefined : this.items.find((item) => item.value === this.value);
59
+ const pressed = new Set(this.values);
45
60
  const entry = this.focusable ? this.entry() : undefined;
46
61
  for (const item of this.items) {
47
62
  item.selected = item === active;
63
+ item.pressed = this.multiple ? pressed.has(item.value) : undefined;
48
64
  item.focusable = item === entry;
49
65
  }
50
66
  }
51
67
  entry() {
52
- const active = this.items.find((item) => item.value === this.value);
53
- return active && !active.disabled ? active : this.items.find((item) => !item.disabled);
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';
54
80
  }
55
81
  get focusTarget() {
56
82
  return (super.focusTarget ??
@@ -64,6 +90,9 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
64
90
  if (this.value === '') {
65
91
  return false;
66
92
  }
93
+ if (this.multiple) {
94
+ return this.children === undefined || this.values.length > 0;
95
+ }
67
96
  if (this.children === undefined) {
68
97
  return true;
69
98
  }
@@ -73,10 +102,12 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
73
102
  connectedCallback() {
74
103
  super.connectedCallback();
75
104
  this.addEventListener('helix-select', this.handleSelection);
105
+ this.addEventListener('focusin', this.noteFocus);
76
106
  }
77
107
  disconnectedCallback() {
78
108
  super.disconnectedCallback();
79
109
  this.removeEventListener('helix-select', this.handleSelection);
110
+ this.removeEventListener('focusin', this.noteFocus);
80
111
  }
81
112
  firstUpdated(changed) {
82
113
  super.firstUpdated(changed);
@@ -92,12 +123,32 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
92
123
  willUpdate(changed) {
93
124
  super.willUpdate(changed);
94
125
  this.adopt();
95
- hostAria(this.internals, { label: this.label, role: 'radiogroup' });
96
- this.internals.setFormValue(this.answered ? this.value : null);
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;
97
142
  }
98
143
  validate() {
99
144
  this.reportRequired(!this.answered, 'Choose one to continue.', 'track');
100
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(',');
151
+ }
101
152
  render() {
102
153
  const inside = this.labelPlacement === 'start';
103
154
  const label = this.renderGroupLabel();
@@ -127,6 +178,15 @@ __decorate([
127
178
  __decorate([
128
179
  property({ attribute: 'label-placement', reflect: true })
129
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);
130
190
  HelixHeadlessToggleGroup = __decorate([
131
191
  customElement('helix-headless-toggle-group')
132
192
  ], HelixHeadlessToggleGroup);
@@ -5,7 +5,9 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
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,6 +25,7 @@ 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 {
@@ -69,6 +72,14 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
69
72
  * React consumer's hydration reports and does not patch up.
70
73
  */
71
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;
72
83
  /**
73
84
  * Asks for the render that follows a hydration, so the stop stops being every
74
85
  * choice's.
@@ -5,7 +5,7 @@ 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
  import { customElement, HelixElement, HostClickController, MixinSelectableItem, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
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() {
@@ -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,8 +30,9 @@ 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
37
  tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
33
38
  @click=${() => this.select()}
@@ -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);
@@ -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
+ };
@@ -10,29 +10,32 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
10
10
  * does not clip it and no `z-index` is needed. `FloatingController` positions
11
11
  * it and flips it to another side when the preferred side does not fit.
12
12
  *
13
- * Wrapping the trigger is the convenient form. Use it where the trigger already
14
- * has an accessible name, because the tooltip cannot describe a slotted
15
- * trigger: an IDREF cannot cross a shadow boundary.
16
- *
17
- * To add a description, place the tooltip beside the trigger and name the
18
- * trigger with `for`:
13
+ * **The tooltip describes its trigger itself, in both forms.** Wrap the
14
+ * trigger, or place the tooltip beside it and name the trigger with `for`:
19
15
  *
20
16
  * ```html
21
- * <helix-headless-button id="save" aria-describedby="save-tip">Save</helix-headless-button>
22
- * <helix-headless-tooltip id="save-tip" for="save" content="Save your work"></helix-headless-tooltip>
17
+ * <helix-headless-button id="save">Save</helix-headless-button>
18
+ * <helix-headless-tooltip for="save" content="Save your work"></helix-headless-tooltip>
23
19
  * ```
24
20
  *
25
- * Write the `aria-describedby` yourself, pointing at the tooltip host. Setting
26
- * it at runtime causes a hydration mismatch that React does not fix. With
27
- * `for`, the tooltip warns once on first open if the attribute is missing.
21
+ * The link is an element reference on the control that takes focus, and it is
22
+ * never written into your markup before a reader has acted. A trigger that is a
23
+ * Helix component is described at once, inside its own shadow root. A plain
24
+ * element in your markup is described when a reader first points at it or
25
+ * focuses it, which is after any hydration, and only where you have written no
26
+ * `aria-describedby` or `aria-labelledby` of your own. Write
27
+ * `aria-describedby` pointing at the tooltip yourself to have the link in the
28
+ * server's markup. Where the browser has no element references, write it
29
+ * yourself; the tooltip warns once on first open if nothing links the two.
28
30
  *
29
31
  * The tooltip shows on hover and on focus. It hides on pointer leave, on blur,
30
32
  * on Escape, and when the trigger is activated. The pointer can move from the
31
33
  * trigger onto the bubble and the bubble stays.
32
34
  *
33
35
  * Once one tooltip is showing, moving to another trigger shows its tooltip
34
- * with no `delay`, and the first one hides. The same holds for 300
35
- * milliseconds after a tooltip hides.
36
+ * with no `delay`, and the first one hides. The same holds for `skip-delay`
37
+ * milliseconds after a tooltip hides, 300 unless you set it. `show-on="focus"`
38
+ * shows the tooltip on keyboard focus only, for a hint a pointer does not need.
36
39
  *
37
40
  * @since 0.1.0
38
41
  * @status stable
@@ -40,9 +43,8 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
40
43
  * @aliases Hint, Popup label
41
44
  * @documentation https://fusion.dev/helix/components/tooltip
42
45
  *
43
- * @slot - The trigger, in the wrapping form. A trigger here gets no
44
- * description, so use it only where the trigger's name says what the tooltip
45
- * says, and use `for` otherwise.
46
+ * @slot - The trigger, in the wrapping form. The tooltip describes it, as it
47
+ * describes the element `for` names.
46
48
  * @slot content - Rich content for the bubble. It replaces the `content`
47
49
  * property.
48
50
  *
@@ -149,6 +151,18 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
149
151
  * It moves if that side does not fit.
150
152
  */
151
153
  placement: Placement;
154
+ /**
155
+ * What shows the bubble. Unset, it is a pointer resting on the trigger and
156
+ * keyboard focus. `focus` is keyboard focus alone, for a hint a pointer does
157
+ * not need, such as a shortcut a mouse reader has no use for.
158
+ */
159
+ showOn?: 'focus';
160
+ /**
161
+ * How long after a tooltip hides the next one still shows at once, with no
162
+ * `delay`, in milliseconds. The same skip delay Radix uses. Zero makes every
163
+ * tooltip wait.
164
+ */
165
+ skipDelay: number;
152
166
  /**
153
167
  * The id of a box to keep the bubble inside. Defaults to the viewport.
154
168
  *
@@ -260,7 +274,7 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
260
274
  * Shows after the pointer has rested for `delay`, or at once while the
261
275
  * tooltips are warm.
262
276
  *
263
- * **Warm is another tooltip showing, or one that hid within `SKIP_DELAY`.**
277
+ * **Warm is another tooltip showing, or one that hid within `skipDelay`.**
264
278
  * A reader scanning a toolbar has already waited once, and waiting again on
265
279
  * every control turns the scan into a series of pauses.
266
280
  */
@@ -344,6 +358,17 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
344
358
  * have upgraded, or existed, when this one connected.
345
359
  */
346
360
  private bind;
361
+ /** The control this tooltip has described, so the link can be taken back. */
362
+ private linked;
363
+ /**
364
+ * Describes the control that takes the trigger's focus by this tooltip.
365
+ *
366
+ * `light` says a reader has just acted, which is what allows a write onto a
367
+ * plain element in the consumer's markup. See `linkDescription`.
368
+ */
369
+ private link;
370
+ /** A reader arriving on the trigger, which is late enough to write the link. */
371
+ private readonly linkOnArrival;
347
372
  private unbind;
348
373
  /** Said once per element, however many times it opens. */
349
374
  private audited;