@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
@@ -19,9 +19,11 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
19
19
  * Focus stays in the text field. The arrow keys move through the list with the
20
20
  * listbox's public `navigate` method. `role="combobox"` is on the input itself.
21
21
  * ArrowDown and ArrowUp open the list on the chosen option, and Enter chooses
22
- * the highlighted one. Space, Home, End and letters edit the text, and Enter in
22
+ * the highlighted one. As typing narrows the list, a screen reader hears how
23
+ * many options are left, such as "3 results". Space, Home, End and letters edit the text, and Enter in
23
24
  * a closed field is left to the form. Emptying the field and leaving it clears
24
- * a single choice; Escape puts the chosen words back.
25
+ * a single choice; Escape puts the chosen words back, and a second Escape on
26
+ * the closed field clears the choice.
25
27
  *
26
28
  * With `multiple`, each chosen option is a tag in the field with its own remove
27
29
  * control. The panel stays open and the filter clears after each choice, so the
@@ -39,11 +41,13 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
39
41
  * @documentation https://fusion.dev/helix/components/combobox
40
42
  *
41
43
  * @slot - The options.
44
+ * @slot empty - What the list shows when typing hides every option, for content richer than `empty-text`.
42
45
  * @slot end - After the text, for a unit or a clear button.
43
46
  * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
44
47
  * @slot hint - The hint, for content richer than plain text.
45
48
  * @slot indicator - Replaces the caret that shows the field opens a list.
46
49
  * @slot label - The label, for content richer than plain text.
50
+ * @slot loading - What the list shows while the options load, for content richer than `loading-text`.
47
51
  * @slot start - Before the text, for an icon.
48
52
  *
49
53
  * @event {Event} change - Fired after the value changes.
@@ -71,16 +75,18 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
71
75
  * @csspart listbox-hint - Help under the list.
72
76
  * @csspart listbox-items - The element that holds the options.
73
77
  * @csspart listbox-label - The label above the list.
74
- * @csspart listbox-live - The inner list's `live` part, the region screen readers announce its empty and loading messages from.
78
+ * @csspart listbox-live - The inner list's `live` part, the region screen readers announce its empty and loading messages, and how many options typing left, from.
75
79
  * @csspart listbox-loading - Shown while the options are loading.
76
80
  * @csspart listbox-search - The region holding the field that narrows the list.
77
81
  * @csspart listbox-search-field - The filter field inside that region.
78
82
  * @csspart rest - The count of choices held beyond `max-tags`. A `<button>` with `expandable`, a `<span>` without it.
79
83
  * @csspart surface - The floating panel.
80
84
  * @csspart tag - One chosen option.
85
+ * @csspart tag-cell - The tag's grid cell, which a tag draws only inside a tag group.
81
86
  * @csspart tag-dismiss - The remove button, when `dismissible`.
82
87
  * @csspart tag-dismiss-mark - The glyph on the dismiss control, where none is slotted.
83
88
  * @csspart tag-label - The label, trimmed to its capitals and baseline.
89
+ * @csspart tag-link - The tag's link around its label, which a tag draws only with `href`.
84
90
  * @csspart tag-start - The category before the label.
85
91
  * @csspart tags - The row of chosen options, when `multiple` is set.
86
92
  *
@@ -89,6 +95,17 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
89
95
  */
90
96
  export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
91
97
  protected readonly aria: AriaForwardController;
98
+ /**
99
+ * Whether text that names no option is taken as the value, as React Aria's
100
+ * `allowsCustomValue`.
101
+ *
102
+ * For a field where the list is a help and not the whole of the answer, a
103
+ * city or a job title. What was typed becomes the value when the reader
104
+ * leaves the field or presses Enter with nothing highlighted, and text that
105
+ * names an option takes that option. Unlike `creatable`, no event asks
106
+ * first and no option is expected to be added: the value is the text.
107
+ */
108
+ allowsCustomValue: boolean;
92
109
  /**
93
110
  * What the browser may autofill in the text field, as a platform token.
94
111
  *
@@ -174,6 +191,17 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
174
191
  * removes rather than as "Remove" said once per chosen option.
175
192
  */
176
193
  removeLabel: string;
194
+ /**
195
+ * What a screen reader hears when typing leaves one option. `{count}` is
196
+ * the count.
197
+ */
198
+ resultLabel: string;
199
+ /**
200
+ * What a screen reader hears when typing leaves any other number of
201
+ * options. `{count}` is the count, and `Intl.PluralRules` picks between the
202
+ * two.
203
+ */
204
+ resultsLabel: string;
177
205
  /**
178
206
  * Whether the field completes the word as the user types.
179
207
  *
@@ -313,6 +341,11 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
313
341
  * once answered.
314
342
  */
315
343
  private commitEmptied;
344
+ /**
345
+ * Takes what was typed as the answer, where `allows-custom-value` asks: the
346
+ * option its words name, or the text itself. Returns whether it took any.
347
+ */
348
+ private commitTyped;
316
349
  /** The options the arrows can reach. */
317
350
  private get reachable();
318
351
  /**
@@ -418,6 +451,8 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
418
451
  * component: a form that received "jav" got the filter, not an option.
419
452
  */
420
453
  private sync;
454
+ /** A link option, once followed, closes the panel as a choice does. */
455
+ private readonly handleLinkFollowed;
421
456
  connectedCallback(): void;
422
457
  disconnectedCallback(): void;
423
458
  protected willUpdate(changed: PropertyValues): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, customElement, fillTemplate, HelixCreateEvent, HelixFilterEvent, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportValues, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, customElement, fillTemplate, HelixCreateEvent, HelixFilterEvent, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportValues, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -21,6 +21,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
21
21
  this.aria = new AriaForwardController(this, {
22
22
  target: () => this.partNamed('control'),
23
23
  });
24
+ this.allowsCustomValue = false;
24
25
  this.autocomplete = 'off';
25
26
  this.creatable = false;
26
27
  this.createLabel = 'Add';
@@ -32,6 +33,8 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
32
33
  this.placeholder = '';
33
34
  this.readOnly = false;
34
35
  this.removeLabel = 'Remove {name}';
36
+ this.resultLabel = '{count} result';
37
+ this.resultsLabel = '{count} results';
35
38
  this.suggest = false;
36
39
  this.typed = '';
37
40
  this.suggestion = '';
@@ -81,6 +84,12 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
81
84
  if (this.open) {
82
85
  event.preventDefault();
83
86
  this.close();
87
+ return;
88
+ }
89
+ if (!this.multiple && this.value !== '' && !this.readOnly) {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+ this.requestValue('');
84
93
  }
85
94
  return;
86
95
  }
@@ -99,6 +108,16 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
99
108
  this.create();
100
109
  return;
101
110
  }
111
+ if (event.key === 'Enter' &&
112
+ this.allowsCustomValue &&
113
+ !this.reachable.some((option) => option.active) &&
114
+ this.commitTyped()) {
115
+ event.preventDefault();
116
+ if (!this.multiple) {
117
+ this.close();
118
+ }
119
+ return;
120
+ }
102
121
  if (!this.open) {
103
122
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
104
123
  event.preventDefault();
@@ -151,6 +170,9 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
151
170
  const tag = event.currentTarget;
152
171
  this.drop(tag.dataset.value ?? '');
153
172
  };
173
+ this.handleLinkFollowed = (event) => {
174
+ afterLinkFollowed(event, () => this.requestOpen(false));
175
+ };
154
176
  }
155
177
  requestValue(next) {
156
178
  if (this.readOnly) {
@@ -246,6 +268,28 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
246
268
  this.requestValue('');
247
269
  }
248
270
  this.emptied = false;
271
+ this.commitTyped();
272
+ }
273
+ commitTyped() {
274
+ const typed = this.typed.trim();
275
+ if (!this.allowsCustomValue || !this.editing || typed === '') {
276
+ return false;
277
+ }
278
+ const named = (this.listbox?.options ?? []).find((option) => option.text.trim().toLowerCase() === typed.toLowerCase());
279
+ const answer = named?.value ?? typed;
280
+ if (this.multiple) {
281
+ if (!this.values.includes(answer)) {
282
+ this.requestValue([...this.values, answer].join(','));
283
+ }
284
+ }
285
+ else if (answer !== this.value) {
286
+ this.requestValue(answer);
287
+ }
288
+ this.typed = '';
289
+ this.suggestion = '';
290
+ this.editing = false;
291
+ this.listbox?.requestFilter('');
292
+ return true;
249
293
  }
250
294
  get reachable() {
251
295
  return (this.listbox?.options ?? []).filter((option) => !option.hidden && !option.disabled);
@@ -312,6 +356,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
312
356
  }
313
357
  connectedCallback() {
314
358
  super.connectedCallback();
359
+ this.addEventListener('helix-select', this.handleLinkFollowed);
315
360
  this.watcher = new MutationObserver(() => this.requestUpdate());
316
361
  this.watcher.observe(this, {
317
362
  attributeFilter: ['label', 'value'],
@@ -322,6 +367,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
322
367
  }
323
368
  disconnectedCallback() {
324
369
  super.disconnectedCallback();
370
+ this.removeEventListener('helix-select', this.handleLinkFollowed);
325
371
  this.watcher?.disconnect();
326
372
  this.watcher = undefined;
327
373
  }
@@ -390,6 +436,8 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
390
436
  aria-label=${this.listLabel}
391
437
  empty-text=${this.emptyText}
392
438
  loading-text=${this.loadingText}
439
+ result-label=${this.resultLabel}
440
+ results-label=${this.resultsLabel}
393
441
  ?loading=${this.loading}
394
442
  ?multiple=${this.multiple}
395
443
  .value=${this.value}
@@ -404,12 +452,17 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
404
452
  property, so nothing else would schedule the update that fixes
405
453
  it. -->
406
454
  <slot @slotchange=${() => this.requestUpdate()}></slot>
455
+ <!-- Handed on to the list's own, with the text as this slot's fallback:
456
+ a slot passed into another is assigned there whether or not anything
457
+ fills it, so the list's fallback would never show. -->
458
+ <slot name="empty" slot="empty">${this.emptyText}</slot>
459
+ <slot name="loading" slot="loading">${this.loadingText}</slot>
407
460
  </${this.tag('listbox')}>`;
408
461
  }
409
462
  renderTag(option) {
410
463
  return html `<${this.tag('tag')}
411
464
  part="tag"
412
- exportparts="dismiss: tag-dismiss, dismiss-mark: tag-dismiss-mark, label: tag-label, start: tag-start"
465
+ exportparts="cell: tag-cell, dismiss: tag-dismiss, dismiss-mark: tag-dismiss-mark, label: tag-label, link: tag-link, start: tag-start"
413
466
  data-value=${option.value}
414
467
  dismiss-label=${fillTemplate(this.removeLabel, { name: option.text })}
415
468
  ?dismissible=${!this.isDisabled && !this.readOnly}
@@ -473,6 +526,9 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
473
526
  ${this.renderHint()}${this.renderFeedback()}`;
474
527
  }
475
528
  };
529
+ __decorate([
530
+ property({ attribute: 'allows-custom-value', type: Boolean })
531
+ ], HelixHeadlessCombobox.prototype, "allowsCustomValue", void 0);
476
532
  __decorate([
477
533
  property()
478
534
  ], HelixHeadlessCombobox.prototype, "autocomplete", void 0);
@@ -506,6 +562,12 @@ __decorate([
506
562
  __decorate([
507
563
  property({ attribute: 'remove-label' })
508
564
  ], HelixHeadlessCombobox.prototype, "removeLabel", void 0);
565
+ __decorate([
566
+ property({ attribute: 'result-label' })
567
+ ], HelixHeadlessCombobox.prototype, "resultLabel", void 0);
568
+ __decorate([
569
+ property({ attribute: 'results-label' })
570
+ ], HelixHeadlessCombobox.prototype, "resultsLabel", void 0);
509
571
  __decorate([
510
572
  property({ type: Boolean })
511
573
  ], HelixHeadlessCombobox.prototype, "suggest", void 0);
@@ -14,7 +14,8 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
14
14
  *
15
15
  * The palette keeps no value. It is not a form control and submits nothing.
16
16
  * Choosing an option dispatches `helix-select` once. To remember the last
17
- * command, store it yourself.
17
+ * command, store it yourself. An option with `href` is a link to a page, which
18
+ * the browser follows once the palette has closed.
18
19
  *
19
20
  * The hotkey listener is on the document, so the hotkey works from anywhere on
20
21
  * the page, including inside another control's text field, whether or not the
@@ -22,7 +23,8 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
22
23
  * write one binding.
23
24
  *
24
25
  * The filter is cleared when the palette closes, so each opening starts from
25
- * the full list. Combobox and Select do the same.
26
+ * the full list. Combobox and Select do the same. As typing narrows the list, a
27
+ * screen reader hears how many commands are left, such as "3 commands".
26
28
  *
27
29
  * @since 0.1.0
28
30
  * @status stable
@@ -31,15 +33,22 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
31
33
  * @documentation https://fusion.dev/helix/components/command
32
34
  *
33
35
  * @slot - The commands, and any groups of them.
36
+ * @slot empty - What the palette shows when typing matches no commands, for content richer than `empty-text`.
34
37
  * @slot footer - Content under the list, such as the keys that work in the palette.
38
+ * @slot loading - What the palette shows while the commands load, for content richer than `loading-text`.
35
39
  *
36
40
  * @event {HelixBeforeCloseEvent} helix-before-close - The palette is about to close. Cancelable: cancel it to keep it open.
37
41
  * @event {HelixBeforeOpenEvent} helix-before-open - The palette is about to open. Cancelable: cancel it to keep it closed.
38
42
  * @event {HelixFilterEvent} helix-filter - Fired as the user types, with the text the list is filtered by. Cancelable: cancel it to leave the list unfiltered and filter the commands yourself, for example on a server while `loading` is set.
39
- * @event {HelixSelectEvent} helix-select - A command was chosen. Cancelable: cancel it to keep the palette open.
43
+ * @event {HelixSelectEvent} helix-select - A command was chosen, with its `href` where the option is a link. Cancelable: cancel it to keep the palette open, and to keep the browser from following the link.
40
44
  * @event {ToggleEvent} toggle - The palette opened or closed. The event carries `newState` and `oldState`.
41
45
  *
42
46
  * @csspart dialog - The box of the dialog the palette opens in, forwarded from `Dialog`.
47
+ * @csspart dialog-body - The dialog's slot for its content, which holds the list. It draws no box until you give it one.
48
+ * @csspart dialog-dismiss - The dialog's close control, drawn when the dialog is `dismissible`.
49
+ * @csspart dialog-dismiss-mark - The glyph on the dialog's close control, where none is slotted.
50
+ * @csspart dialog-footer - The dialog's slot for its actions, which holds the `footer` row. It draws no box until you give it one.
51
+ * @csspart dialog-header - The dialog's slot for its title. It draws no box until you give it one.
43
52
  * @csspart footer - The row under the list.
44
53
  * @csspart listbox - The listbox inside the palette.
45
54
  * @csspart listbox-empty - The text shown when the filter matches no commands.
@@ -49,7 +58,7 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
49
58
  * @csspart listbox-hint - Help text under the list.
50
59
  * @csspart listbox-items - The list of commands.
51
60
  * @csspart listbox-label - The label above the list.
52
- * @csspart listbox-live - The listbox's `live` part, a live region that holds the empty and loading text for screen readers.
61
+ * @csspart listbox-live - The listbox's `live` part, a live region that holds the empty and loading text, and how many commands typing left, for screen readers.
53
62
  * @csspart listbox-loading - The text shown while the commands are loading.
54
63
  * @csspart listbox-search - The region that holds the filter field.
55
64
  * @csspart listbox-search-field - The filter field.
@@ -61,6 +70,12 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
61
70
  * The text shown when the filter matches no commands.
62
71
  */
63
72
  emptyText: string;
73
+ /**
74
+ * How long typing rests before the commands are filtered, in milliseconds,
75
+ * so a palette filtered on a server sends one `helix-filter` for a word.
76
+ * `0` filters on every keystroke. See Listbox's `filter-delay`.
77
+ */
78
+ filterDelay: number;
64
79
  /**
65
80
  * The key that opens the palette, written as `mod+k`, `shift+p`, or `k`.
66
81
  *
@@ -88,6 +103,17 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
88
103
  * The text shown in the filter field before anything is typed.
89
104
  */
90
105
  placeholder: string;
106
+ /**
107
+ * What a screen reader hears when typing leaves one command. `{count}` is
108
+ * the count.
109
+ */
110
+ resultLabel: string;
111
+ /**
112
+ * What a screen reader hears when typing leaves any other number of
113
+ * commands. `{count}` is the count, and `Intl.PluralRules` picks between the
114
+ * two.
115
+ */
116
+ resultsLabel: string;
91
117
  /**
92
118
  * Whether the consumer put anything under the list.
93
119
  *
@@ -22,11 +22,14 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
22
22
  constructor() {
23
23
  super(...arguments);
24
24
  this.emptyText = 'No commands';
25
+ this.filterDelay = 0;
25
26
  this.hotkey = 'mod+k';
26
27
  this.label = 'Commands';
27
28
  this.loading = false;
28
29
  this.loadingText = 'Loading';
29
30
  this.placeholder = 'Search commands';
31
+ this.resultLabel = '{count} command';
32
+ this.resultsLabel = '{count} commands';
30
33
  this.footed = false;
31
34
  this.handleHotkey = (event) => {
32
35
  if (this.hotkey === '') {
@@ -73,9 +76,19 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
73
76
  }
74
77
  };
75
78
  this.swallowOptionSelect = (event) => {
76
- if (event.target !== this) {
77
- event.stopImmediatePropagation();
79
+ if (event.target === this) {
80
+ return;
81
+ }
82
+ event.stopImmediatePropagation();
83
+ const { href, value } = event.detail;
84
+ if (href === undefined) {
85
+ return;
86
+ }
87
+ if (!this.dispatchEvent(new HelixSelectEvent({ href, value }))) {
88
+ event.preventDefault();
89
+ return;
78
90
  }
91
+ this.requestOpen(false);
79
92
  };
80
93
  }
81
94
  get listbox() {
@@ -125,7 +138,8 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
125
138
  }
126
139
  renderDialog() {
127
140
  return html `<${this.tag('dialog')}
128
- exportparts="dialog"
141
+ exportparts="body: dialog-body, dialog, dismiss: dialog-dismiss, dismiss-mark: dialog-dismiss-mark,
142
+ footer: dialog-footer, header: dialog-header"
129
143
  label=${this.label}
130
144
  .open=${this.open}
131
145
  @helix-before-close=${() => {
@@ -142,8 +156,11 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
142
156
  search-field: listbox-search-field"
143
157
  aria-label=${this.label}
144
158
  empty-text=${this.emptyText}
159
+ filter-delay=${this.filterDelay}
145
160
  filter-label=${this.placeholder}
146
161
  loading-text=${this.loadingText}
162
+ result-label=${this.resultLabel}
163
+ results-label=${this.resultsLabel}
147
164
  ?loading=${this.loading}
148
165
  filterable
149
166
  @helix-before-change=${this.handleChange}
@@ -151,6 +168,10 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
151
168
  @input=${this.stopInner}
152
169
  >
153
170
  <slot></slot>
171
+ <!-- Handed on with the text as the fallback, for the reason Combobox
172
+ gives: the list's own fallback never shows behind a passed slot. -->
173
+ <slot name="empty" slot="empty">${this.emptyText}</slot>
174
+ <slot name="loading" slot="loading">${this.loadingText}</slot>
154
175
  </${this.tag('listbox')}>
155
176
  <div ?hidden=${!this.footed} part="footer" slot="footer">
156
177
  <slot
@@ -169,6 +190,9 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
169
190
  __decorate([
170
191
  property({ attribute: 'empty-text' })
171
192
  ], HelixHeadlessCommand.prototype, "emptyText", void 0);
193
+ __decorate([
194
+ property({ attribute: 'filter-delay', type: Number })
195
+ ], HelixHeadlessCommand.prototype, "filterDelay", void 0);
172
196
  __decorate([
173
197
  property()
174
198
  ], HelixHeadlessCommand.prototype, "hotkey", void 0);
@@ -184,6 +208,12 @@ __decorate([
184
208
  __decorate([
185
209
  property()
186
210
  ], HelixHeadlessCommand.prototype, "placeholder", void 0);
211
+ __decorate([
212
+ property({ attribute: 'result-label' })
213
+ ], HelixHeadlessCommand.prototype, "resultLabel", void 0);
214
+ __decorate([
215
+ property({ attribute: 'results-label' })
216
+ ], HelixHeadlessCommand.prototype, "resultsLabel", void 0);
187
217
  __decorate([
188
218
  state()
189
219
  ], HelixHeadlessCommand.prototype, "footed", void 0);
@@ -0,0 +1,22 @@
1
+ /** What a `from` reference names: an element by its id, and what to read from it. */
2
+ export interface FromReference {
3
+ /** An attribute to read, written `id[attribute]`. */
4
+ readonly attribute?: string;
5
+ /** The id of the element. */
6
+ readonly id: string;
7
+ /** A property to read, written `id.property`. */
8
+ readonly property?: string;
9
+ }
10
+ /**
11
+ * Reads a `from` reference: `id` for the element's text, `id[attribute]` for
12
+ * an attribute, and `id.property` for a property. Web Awesome's Copy Button
13
+ * takes the same three shapes.
14
+ *
15
+ * `null` for a reference with no id, or with anything after the brackets.
16
+ */
17
+ export declare const parseFrom: (reference: string) => FromReference | null;
18
+ /**
19
+ * The text a `from` reference names, looked up in the tree `scope` is in, or
20
+ * `null` where the element or its attribute is not there.
21
+ */
22
+ export declare const readFrom: (reference: string, scope: Element) => string | null;
@@ -0,0 +1,28 @@
1
+ import { elementById } from '@fusion.dev/helix-core';
2
+ export const parseFrom = (reference) => {
3
+ const written = reference.trim();
4
+ const bracket = /^([^[\].\s]+)\[([^\]\s]+)\]$/u.exec(written);
5
+ if (bracket) {
6
+ return { attribute: bracket[2], id: bracket[1] };
7
+ }
8
+ const dot = /^([^[\].\s]+)\.([^[\].\s]+)$/u.exec(written);
9
+ if (dot) {
10
+ return { id: dot[1], property: dot[2] };
11
+ }
12
+ return /^[^[\].\s]+$/u.test(written) ? { id: written } : null;
13
+ };
14
+ export const readFrom = (reference, scope) => {
15
+ const parsed = parseFrom(reference);
16
+ const element = parsed ? elementById(scope, parsed.id) : null;
17
+ if (!(parsed && element)) {
18
+ return null;
19
+ }
20
+ if (parsed.attribute !== undefined) {
21
+ return element.getAttribute(parsed.attribute);
22
+ }
23
+ if (parsed.property !== undefined) {
24
+ const read = element[parsed.property];
25
+ return read === undefined || read === null ? null : String(read);
26
+ }
27
+ return element.textContent;
28
+ };
@@ -12,7 +12,13 @@ import '../../tooltip/index.js';
12
12
  * The confirmation and the event wait for the write to finish. The browser can
13
13
  * refuse a clipboard write, for example on an insecure origin, in a document
14
14
  * that is not focused, or after a gesture that does not qualify. A refused
15
- * write leaves the button unchanged.
15
+ * write, or nothing to copy, names the button with `error-label` for the same
16
+ * moment a copy shows `copied-label`, draws a cross, and sends
17
+ * `helix-copy-error`.
18
+ *
19
+ * `from` copies from another element on the page: its text, an attribute
20
+ * written `id[attribute]`, or a property written `id.property`, read at the
21
+ * press. It takes the place of `value`.
16
22
  *
17
23
  * It replaces Button's render and its press, so it never submits a form or
18
24
  * sends a command: `type`, `command`, `commandfor` and the `form*` overrides
@@ -28,10 +34,13 @@ import '../../tooltip/index.js';
28
34
  *
29
35
  * @slot - The icon shown before a copy. Defaults to a clipboard.
30
36
  * @slot copied - The icon shown after a successful copy. Defaults to a check mark.
37
+ * @slot failed - The icon shown after a copy that put nothing on the clipboard. Defaults to a cross.
31
38
  *
32
39
  * @event {HelixCopyEvent} helix-copy - Fired after the clipboard write succeeds.
40
+ * @event {Event} helix-copy-error - Fired when a press put nothing on the clipboard: the browser refused the write, or there was nothing to copy.
33
41
  *
34
42
  * @csspart button - The inner button.
43
+ * @csspart live - The live region that says `pending-label` when the button becomes pending under focus. It is rendered from the first time the button is pending.
35
44
  * @csspart mark - The clipboard icon, and the confirmation icon that replaces it.
36
45
  * @csspart tooltip - The tooltip bubble, forwarded from the tooltip inside the button.
37
46
  * @csspart tooltip-arrow - The tooltip's arrow, which points at the button.
@@ -52,6 +61,18 @@ export declare class HelixHeadlessCopyButton extends HelixHeadlessIconButton {
52
61
  * dash-casing it, and `copiedlabel` is not what anyone would type.
53
62
  */
54
63
  copiedLabel: string;
64
+ /**
65
+ * The button's name and tooltip text after a press that put nothing on the
66
+ * clipboard.
67
+ */
68
+ errorLabel: string;
69
+ /**
70
+ * The element to copy from, by its id: `key` for its text, `key[value]` for
71
+ * an attribute, and `key.value` for a property. Read at the press, and used
72
+ * in place of `value`. An element that is not there copies nothing, which is
73
+ * a failed copy.
74
+ */
75
+ from: string;
55
76
  /**
56
77
  * The button's name and tooltip text before a copy.
57
78
  *
@@ -107,6 +128,9 @@ export declare class HelixHeadlessCopyButton extends HelixHeadlessIconButton {
107
128
  */
108
129
  private readonly clipboard;
109
130
  private get copied();
131
+ private get failed();
132
+ /** What a press copies: the text `from` names, or `value`. */
133
+ private get text();
110
134
  private readonly handleCopy;
111
135
  /**
112
136
  * Both faces, drawn rather than demanded.
@@ -139,6 +163,8 @@ export declare class HelixHeadlessCopyButton extends HelixHeadlessIconButton {
139
163
  * the focused element mid-press.
140
164
  */
141
165
  protected renderCopiedIcon(): TemplateResult;
166
+ /** The mark for a press that put nothing on the clipboard, behind a seam of its own. */
167
+ protected renderFailedIcon(): TemplateResult;
142
168
  /**
143
169
  * The bubble the control sits inside, behind a seam.
144
170
  *
@@ -6,30 +6,44 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  var HelixHeadlessCopyButton_1;
8
8
  import { CopyController, customElement } from '@fusion.dev/helix-core';
9
- import { check, copy } from '@fusion.dev/helix-icons';
9
+ import { check, copy, x } from '@fusion.dev/helix-icons';
10
10
  import { nothing } from 'lit';
11
11
  import { property } from 'lit/decorators.js';
12
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
13
  import { html } from 'lit/static-html.js';
14
14
  import { HelixHeadlessIconButton } from '../../icon-button/root/icon-button-root.js';
15
+ import { readFrom } from '../from.js';
15
16
  import '../../tooltip/index.js';
16
17
  const CONFIRM = 1600;
17
18
  let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCopyButton extends HelixHeadlessIconButton {
18
19
  constructor() {
19
20
  super(...arguments);
20
21
  this.copiedLabel = 'Copied';
22
+ this.errorLabel = 'Could not copy';
23
+ this.from = '';
21
24
  this.label = 'Copy';
22
25
  this.value = '';
23
26
  this.namedBy = null;
24
27
  this.clipboard = new CopyController(this, { after: CONFIRM });
25
- this.handleCopy = async () => {
28
+ this.handleCopy = async (event) => {
29
+ if (this.pending) {
30
+ event.preventDefault();
31
+ event.stopImmediatePropagation();
32
+ return;
33
+ }
26
34
  if (this.isDisabled) {
27
35
  return;
28
36
  }
29
- await this.clipboard.copy(this.value);
37
+ const wrote = await this.clipboard.copy(this.text);
38
+ if (!wrote && this.isConnected) {
39
+ this.dispatchEvent(new Event('helix-copy-error', { bubbles: true, composed: true }));
40
+ }
30
41
  };
31
42
  }
32
43
  get says() {
44
+ if (this.failed) {
45
+ return this.errorLabel;
46
+ }
33
47
  return this.copied ? this.copiedLabel : this.namedBy || this.label;
34
48
  }
35
49
  get forwardsLabel() {
@@ -41,32 +55,48 @@ let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCop
41
55
  get copied() {
42
56
  return this.clipboard.copied;
43
57
  }
58
+ get failed() {
59
+ return this.clipboard.failed;
60
+ }
61
+ get text() {
62
+ return this.from === '' ? this.value : (readFrom(this.from, this) ?? '');
63
+ }
44
64
  static icon(glyph) {
45
65
  return html `<svg part="mark" fill="currentColor" viewBox=${glyph.viewBox} aria-hidden="true">
46
66
  ${unsafeSVG(glyph.content)}
47
67
  </svg>`;
48
68
  }
49
69
  render() {
50
- return this.renderTooltip(html `<button
70
+ return html `${this.renderTooltip(html `<button
51
71
  part="button"
52
72
  type="button"
73
+ aria-busy=${this.pending ? 'true' : nothing}
74
+ aria-disabled=${this.pending ? 'true' : nothing}
53
75
  aria-label=${this.says}
54
76
  data-copied=${this.copied ? '' : nothing}
77
+ data-failed=${this.failed ? '' : nothing}
55
78
  tabindex=${this.focusable ? nothing : '-1'}
56
79
  ?disabled=${this.isDisabled}
57
80
  @click=${this.handleCopy}
58
81
  >
59
- ${this.renderCopiedIcon()} ${this.renderCopyIcon()}
60
- </button>`);
82
+ ${this.renderCopiedIcon()} ${this.renderFailedIcon()} ${this.renderCopyIcon()}
83
+ </button>`)}${this.renderLive()}`;
61
84
  }
62
85
  renderCopyIcon() {
63
- return html `<slot ?hidden=${this.copied}>${HelixHeadlessCopyButton_1.icon(copy)}</slot>`;
86
+ return html `<slot ?hidden=${this.copied || this.failed}
87
+ >${HelixHeadlessCopyButton_1.icon(copy)}</slot
88
+ >`;
64
89
  }
65
90
  renderCopiedIcon() {
66
91
  return html `<slot name="copied" ?hidden=${!this.copied}
67
92
  >${HelixHeadlessCopyButton_1.icon(check)}</slot
68
93
  >`;
69
94
  }
95
+ renderFailedIcon() {
96
+ return html `<slot name="failed" ?hidden=${!this.failed}
97
+ >${HelixHeadlessCopyButton_1.icon(x)}</slot
98
+ >`;
99
+ }
70
100
  renderTooltip(control) {
71
101
  return html `<${this.tag('tooltip')} content=${this.says} persist
72
102
  exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom,
@@ -80,6 +110,12 @@ let HelixHeadlessCopyButton = HelixHeadlessCopyButton_1 = class HelixHeadlessCop
80
110
  __decorate([
81
111
  property({ attribute: 'copied-label' })
82
112
  ], HelixHeadlessCopyButton.prototype, "copiedLabel", void 0);
113
+ __decorate([
114
+ property({ attribute: 'error-label' })
115
+ ], HelixHeadlessCopyButton.prototype, "errorLabel", void 0);
116
+ __decorate([
117
+ property()
118
+ ], HelixHeadlessCopyButton.prototype, "from", void 0);
83
119
  __decorate([
84
120
  property()
85
121
  ], HelixHeadlessCopyButton.prototype, "label", void 0);