@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
@@ -1,5 +1,5 @@
1
1
  import { AriaForwardController, DisabledCascadeController, HelixFormElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { HelixHeadlessOption } from '../../option/root/option-root.js';
4
4
  declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
5
5
  /**
@@ -24,10 +24,18 @@ declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusio
24
24
  * letters move it to the next option whose words start with them. In a
25
25
  * single-choice list the highlight is the choice unless `manual` is set; then
26
26
  * Enter, Space or a click chooses. With `multiple`, Space toggles the
27
- * highlighted option, Shift with an arrow adds the option it moves to, and
27
+ * highlighted option, Shift with an arrow adds the option it moves to, Shift
28
+ * with Home or End adds every option from the highlight to that end, and
28
29
  * Control+A or Command+A chooses every option, or none where all are chosen.
29
30
  * In a filter field, Space, Home, End and letters edit the text.
30
31
  *
32
+ * An option with `href` is a link: Enter or a press follows it and selects
33
+ * nothing, and moving onto it with the arrows does not follow it.
34
+ *
35
+ * As a filter narrows the list, a screen reader hears how many options are
36
+ * left, such as "3 results", from `result-label` and `results-label`. A filter
37
+ * that leaves none says `empty-text`.
38
+ *
31
39
  * @since 0.1.0
32
40
  * @status stable
33
41
  * @category forms
@@ -35,10 +43,12 @@ declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusio
35
43
  * @documentation https://fusion.dev/helix/components/listbox
36
44
  *
37
45
  * @slot - The options, and any groups of them.
46
+ * @slot empty - What the list shows when it has no options to show, for content richer than `empty-text`.
38
47
  * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
39
48
  * @slot filter-icon - The icon in the filter field. Defaults to a magnifying glass.
40
49
  * @slot hint - The hint, for content richer than plain text.
41
50
  * @slot label - The label, for content richer than plain text.
51
+ * @slot loading - What the list shows while its options load, for content richer than `loading-text`.
42
52
  *
43
53
  * @event {Event} change - Fired after the value changes.
44
54
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired when the value is about to change, and cancelable.
@@ -52,12 +62,14 @@ declare const HelixHeadlessListbox_base: (new (...args: any[]) => import("@fusio
52
62
  * @csspart hint - Help under the list.
53
63
  * @csspart items - The list of options.
54
64
  * @csspart label - The label above the list.
55
- * @csspart live - A copy of the empty and loading text, in a region that screen readers announce when it changes. Visually hidden in the styled tier.
65
+ * @csspart live - A copy of the empty and loading text, and how many options a filter left, in a region that screen readers announce when it changes. Visually hidden in the styled tier.
56
66
  * @csspart loading - The text shown while the options load.
57
67
  * @csspart search - The region that holds the filter field.
58
68
  * @csspart search-field - The filter field.
59
69
  *
70
+ * @cssstate empty - The list has no options to show: none were given, or the filter hid every one.
60
71
  * @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
72
+ * @cssstate loading - While `loading` is set.
61
73
  */
62
74
  export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
63
75
  protected readonly aria: AriaForwardController;
@@ -104,6 +116,16 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
104
116
  * own search boxes would be three copies of one field.
105
117
  */
106
118
  filterable: boolean;
119
+ /**
120
+ * How long typing in the filter field rests before the list is filtered, in
121
+ * milliseconds. `0` filters on every keystroke.
122
+ *
123
+ * For a list filtered on a server, where `helix-filter` starts a request: a
124
+ * reader typing a word sends one request for the word, not one for each
125
+ * letter. Enter and the arrow keys filter at once on what is typed, so they
126
+ * act on the list the reader expects.
127
+ */
128
+ filterDelay: number;
107
129
  /**
108
130
  * Placeholder text for the filter field.
109
131
  */
@@ -121,6 +143,20 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
121
143
  * Whether more than one option can be chosen.
122
144
  */
123
145
  multiple: boolean;
146
+ /**
147
+ * What a screen reader hears when a filter leaves one option. `{count}` is
148
+ * the count.
149
+ */
150
+ resultLabel: string;
151
+ /**
152
+ * What a screen reader hears when a filter leaves any other number of
153
+ * options. `{count}` is the count.
154
+ *
155
+ * **Two sentences, chosen by `Intl.PluralRules`**, which is Data Table's
156
+ * pair: a language with more forms than two picks the one for its `one` and
157
+ * writes the rest here.
158
+ */
159
+ resultsLabel: string;
124
160
  /**
125
161
  * Which option the keyboard is on, which is not what is chosen.
126
162
  */
@@ -189,6 +225,18 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
189
225
  * ArrowUp, and Combobox moves the highlight off the list onto its create row.
190
226
  */
191
227
  highlight(where: 'first' | 'last' | 'none' | 'selected'): void;
228
+ /**
229
+ * Says how many options a filter left, in the live region.
230
+ *
231
+ * **A reader typing into a filter cannot see the list narrow**, and React
232
+ * Aria and Web Awesome both say the count as it changes. Only "No matches"
233
+ * was said here, so a query that left forty options and one that left one
234
+ * sounded the same. Said when the filter changes and the count with it, so
235
+ * a keystroke that leaves the same options says nothing new. An empty filter
236
+ * says nothing, being the list as it was given; no matches is `empty-text`,
237
+ * which the region says in place of this.
238
+ */
239
+ private tell;
192
240
  /**
193
241
  * @internal Moves the highlight to the option whose words start with the
194
242
  * letters typed, and returns its value, or nothing where none matches.
@@ -376,6 +424,12 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
376
424
  * their answer moved when it did not is worse than telling them nothing.
377
425
  */
378
426
  private readonly handleFilter;
427
+ /** The filter waiting out `filterDelay`, if any. */
428
+ private readonly filtering;
429
+ /** Filters on what the field holds, and puts the field back where that was refused. */
430
+ private takeFilter;
431
+ /** Filters at once on a filter still waiting out `filterDelay`. */
432
+ private flushFilter;
379
433
  /** Requests a new filter, and returns whether the request went through. */
380
434
  requestFilter(next: string): boolean;
381
435
  /**
@@ -414,6 +468,23 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
414
468
  * cannot change.
415
469
  */
416
470
  protected renderFilterIcon(): TemplateResult;
471
+ /**
472
+ * Whether there is anything to show.
473
+ *
474
+ * Showing nothing is not the same as empty. A filter that hides every option
475
+ * is empty, and one with no filter and no options is empty too, but a list
476
+ * still waiting on its options is neither.
477
+ */
478
+ private get showsOptions();
479
+ /**
480
+ * The empty or loading line, with a slot for each so either can be richer
481
+ * than its text.
482
+ *
483
+ * **The text is the slot's fallback**, which is this library's rule for a
484
+ * content property, so `empty-text` still shows where nothing is slotted and
485
+ * the live region still says it.
486
+ */
487
+ protected renderStatus(hasContent: boolean): TemplateResult | typeof nothing;
417
488
  protected render(): TemplateResult;
418
489
  }
419
490
  declare global {
@@ -4,13 +4,14 @@ 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, DisabledCascadeController, HelixFilterEvent, HelixFormElement, hostAria, MixinCollection, MixinControllerValue, MixinField, reportValues, requestChange, scrollItemIntoView, Typeahead, typesCharacter, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, countLabel, customElement, DelayController, DisabledCascadeController, HelixFilterEvent, HelixFormElement, hostAria, MixinCollection, MixinControllerValue, MixinField, reportValues, requestChange, scrollItemIntoView, Typeahead, typesCharacter, } from '@fusion.dev/helix-core';
8
8
  import { magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { HelixHeadlessOptionGroup } from '../../option/group/option-group.js';
13
13
  import { HelixHeadlessOption } from '../../option/root/option-root.js';
14
+ import { rangeAdd } from '../range.js';
14
15
  const MOVES = new Set(['ArrowDown', 'ArrowUp', 'End', 'Home', 'PageDown', 'PageUp']);
15
16
  const FIELD_MOVES = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
16
17
  const PAGE = 10;
@@ -68,9 +69,12 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
68
69
  this.emptyText = 'No matches';
69
70
  this.filterState = undefined;
70
71
  this.filterable = false;
72
+ this.filterDelay = 0;
71
73
  this.filterLabel = 'Filter';
72
74
  this.manual = false;
73
75
  this.multiple = false;
76
+ this.resultLabel = '{count} result';
77
+ this.resultsLabel = '{count} results';
74
78
  this.activeValue = '';
75
79
  this.parked = false;
76
80
  this.typeahead = new Typeahead();
@@ -82,6 +86,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
82
86
  items: () => this.options,
83
87
  });
84
88
  this.handleSelection = (event) => {
89
+ if (event.detail.href !== undefined) {
90
+ return;
91
+ }
85
92
  event.stopPropagation();
86
93
  if (event.defaultPrevented) {
87
94
  return;
@@ -96,7 +103,14 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
96
103
  }
97
104
  const typing = this.forwarded || event.composedPath()[0] === this.partNamed('search-field');
98
105
  if (event.key === 'Enter' || (event.key === ' ' && !typing && !this.typeahead.typing())) {
99
- if (this.activeValue !== '') {
106
+ const link = shown.find((option) => option.value === this.activeValue && Boolean(option.href));
107
+ if (link !== undefined) {
108
+ event.preventDefault();
109
+ if (event.key === 'Enter') {
110
+ link.select(event);
111
+ }
112
+ }
113
+ else if (this.activeValue !== '') {
100
114
  event.preventDefault();
101
115
  this.choose(this.activeValue);
102
116
  }
@@ -155,6 +169,14 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
155
169
  const target = shown[next];
156
170
  if (target) {
157
171
  this.arrive(target.value);
172
+ const ranged = event.key === 'Home' || event.key === 'End';
173
+ if (this.multiple && !typing && event.shiftKey && ranged) {
174
+ const taken = rangeAdd(this.values, shown.map((option) => option.value), at, next);
175
+ if (taken) {
176
+ this.requestValue(taken.join(','));
177
+ }
178
+ return;
179
+ }
158
180
  if (this.multiple && !typing && event.shiftKey && !this.values.includes(target.value)) {
159
181
  this.choose(target.value);
160
182
  }
@@ -163,16 +185,27 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
163
185
  this.handleFilter = (event) => {
164
186
  event.stopPropagation();
165
187
  const control = event.target;
166
- this.requestFilter(control.value);
167
- if (control.value !== this.filter) {
168
- control.value = this.filter;
188
+ this.filtering.cancel();
189
+ if (this.filterDelay > 0) {
190
+ this.filtering.start(this.filterDelay, () => this.takeFilter(control));
191
+ return;
169
192
  }
193
+ this.takeFilter(control);
170
194
  };
195
+ this.filtering = new DelayController(this, { renders: false });
171
196
  this.handleFilterKeydown = (event) => {
172
- if (event.key === 'Escape' && this.filter !== '') {
197
+ if (event.key === 'Enter' || FIELD_MOVES.has(event.key)) {
198
+ this.flushFilter();
199
+ }
200
+ if (event.key === 'Escape' && (this.filter !== '' || this.filtering.pending)) {
173
201
  event.stopPropagation();
174
202
  event.preventDefault();
203
+ this.filtering.cancel();
175
204
  this.requestFilter('');
205
+ const control = this.partNamed('search-field');
206
+ if (control && control.value !== this.filter) {
207
+ control.value = this.filter;
208
+ }
176
209
  return;
177
210
  }
178
211
  if (!FIELD_MOVES.has(event.key)) {
@@ -218,6 +251,17 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
218
251
  this.place();
219
252
  }
220
253
  }
254
+ tell(changed) {
255
+ if (!changed.has('filter') || !this.hasUpdated) {
256
+ return;
257
+ }
258
+ const count = this.shown.length;
259
+ if (this.filter.trim() === '' || count === 0) {
260
+ this.announced = '';
261
+ return;
262
+ }
263
+ this.announced = countLabel(count, this.resultLabel, this.resultsLabel, this.lang);
264
+ }
221
265
  typeaheadTo(key) {
222
266
  const shown = this.shown.filter((option) => !option.disabled);
223
267
  const from = shown.findIndex((option) => option.value === this.currentValue);
@@ -294,7 +338,8 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
294
338
  arrive(value) {
295
339
  this.parked = false;
296
340
  this.activeValue = value;
297
- if (!this.multiple && !this.manual && !this.forwarded) {
341
+ const link = this.options.some((option) => option.value === value && Boolean(option.href));
342
+ if (!this.multiple && !this.manual && !this.forwarded && !link) {
298
343
  this.choose(value);
299
344
  }
300
345
  }
@@ -423,7 +468,10 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
423
468
  }
424
469
  if (this.childrenKnown) {
425
470
  this.place();
471
+ this.tell(changed);
426
472
  }
473
+ this.toggleState('empty', !this.showsOptions);
474
+ this.toggleState('loading', this.loading);
427
475
  }
428
476
  seedDeclared() {
429
477
  if (this.owned || this.defaultValue !== '' || this.hasAttribute('default-value')) {
@@ -453,6 +501,16 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
453
501
  this.sync();
454
502
  this.linkActive();
455
503
  }
504
+ takeFilter(control) {
505
+ this.filtering.cancel();
506
+ this.requestFilter(control.value);
507
+ if (control.value !== this.filter) {
508
+ control.value = this.filter;
509
+ }
510
+ }
511
+ flushFilter() {
512
+ this.filtering.flush();
513
+ }
456
514
  requestFilter(next) {
457
515
  if (next === this.filter) {
458
516
  return true;
@@ -480,12 +538,23 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
480
538
  </svg>
481
539
  </slot>`;
482
540
  }
483
- render() {
484
- const shown = this.counted && !this.loading
541
+ get showsOptions() {
542
+ return this.counted && !this.loading
485
543
  ? this.filter === ''
486
544
  ? this.options.length > 0
487
545
  : this.hasMatches
488
546
  : true;
547
+ }
548
+ renderStatus(hasContent) {
549
+ if (!this.loading && hasContent) {
550
+ return nothing;
551
+ }
552
+ return this.loading
553
+ ? html `<p part="loading"><slot name="loading">${this.loadingText}</slot></p>`
554
+ : html `<p part="empty"><slot name="empty">${this.emptyText}</slot></p>`;
555
+ }
556
+ render() {
557
+ const shown = this.showsOptions;
489
558
  return html `${this.renderGroupLabel()}
490
559
  ${this.filterable
491
560
  ? html `<div part="search">
@@ -542,6 +611,9 @@ __decorate([
542
611
  __decorate([
543
612
  property({ attribute: 'filterable', reflect: true, type: Boolean })
544
613
  ], HelixHeadlessListbox.prototype, "filterable", void 0);
614
+ __decorate([
615
+ property({ attribute: 'filter-delay', type: Number })
616
+ ], HelixHeadlessListbox.prototype, "filterDelay", void 0);
545
617
  __decorate([
546
618
  property({ attribute: 'filter-label' })
547
619
  ], HelixHeadlessListbox.prototype, "filterLabel", void 0);
@@ -551,6 +623,12 @@ __decorate([
551
623
  __decorate([
552
624
  property({ reflect: true, type: Boolean })
553
625
  ], HelixHeadlessListbox.prototype, "multiple", void 0);
626
+ __decorate([
627
+ property({ attribute: 'result-label' })
628
+ ], HelixHeadlessListbox.prototype, "resultLabel", void 0);
629
+ __decorate([
630
+ property({ attribute: 'results-label' })
631
+ ], HelixHeadlessListbox.prototype, "resultsLabel", void 0);
554
632
  __decorate([
555
633
  state()
556
634
  ], HelixHeadlessListbox.prototype, "activeValue", void 0);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Marks a region busy for as long as something is loading into it, and
3
+ * counts, so two loaders in one region do not clear each other.
4
+ *
5
+ * Beside the family's `index.ts`, because which element is busy and how many
6
+ * claims it holds is bookkeeping about elements, not about a loader's
7
+ * template.
8
+ */
9
+ /**
10
+ * Adds a claim on the region, setting `aria-busy="true"` on the first.
11
+ *
12
+ * Only an `aria-busy` this file wrote is ever taken off again: a region the
13
+ * page marked busy itself keeps its own attribute.
14
+ */
15
+ export declare const claimBusy: (region: Element) => void;
16
+ /** Takes a claim back, and the attribute with the last one. */
17
+ export declare const releaseBusy: (region: Element) => void;
@@ -0,0 +1,24 @@
1
+ const CLAIMS = new WeakMap();
2
+ export const claimBusy = (region) => {
3
+ const held = CLAIMS.get(region) ?? 0;
4
+ if (held === 0 && region.getAttribute('aria-busy') === 'true') {
5
+ return;
6
+ }
7
+ CLAIMS.set(region, held + 1);
8
+ if (held === 0) {
9
+ region.setAttribute('aria-busy', 'true');
10
+ }
11
+ };
12
+ export const releaseBusy = (region) => {
13
+ const held = CLAIMS.get(region);
14
+ if (held === undefined) {
15
+ return;
16
+ }
17
+ if (held <= 1) {
18
+ CLAIMS.delete(region);
19
+ region.removeAttribute('aria-busy');
20
+ }
21
+ else {
22
+ CLAIMS.set(region, held - 1);
23
+ }
24
+ };
@@ -19,6 +19,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
19
19
  * a `role` attribute on the host would be missing from React server-rendered
20
20
  * markup.
21
21
  *
22
+ * **`for` names the region being loaded**, and the loader sets
23
+ * `aria-busy="true"` on it for as long as the loader is on the page, so a
24
+ * screen reader waits for the region rather than reading it half filled. Two
25
+ * loaders in one region hold it busy until the last one leaves.
26
+ *
27
+ * **`delay` keeps a quick load from flashing a loader.** The loader stays
28
+ * hidden, and silent, for that many milliseconds, and appears only if the work
29
+ * is still going.
30
+ *
22
31
  * @since 0.1.0
23
32
  * @status stable
24
33
  * @category feedback
@@ -33,11 +42,39 @@ import { type PropertyValues, type TemplateResult } from 'lit';
33
42
  * @csspart track - The full circle behind the indicator.
34
43
  */
35
44
  export declare class HelixHeadlessLoader extends HelixElement {
45
+ /**
46
+ * How long the loader waits before it shows, in milliseconds, so work that
47
+ * finishes quickly never flashes one.
48
+ *
49
+ * Absent shows it at once. Hidden in the served markup too, so a page that
50
+ * loads quickly never draws it.
51
+ */
52
+ delay: number;
53
+ /**
54
+ * The id of the region being loaded, which is marked `aria-busy` while the
55
+ * loader is on the page.
56
+ *
57
+ * Names an element in the same tree. In React it arrives as a property, the
58
+ * way `Tooltip.for` does.
59
+ */
60
+ for: string;
36
61
  /**
37
62
  * The loader's name, announced by screen readers while the work is in
38
63
  * progress.
39
64
  */
40
65
  label: string;
66
+ /** The wait `delay` asks for, and whether it has run out. */
67
+ private readonly due;
68
+ /** The region this loader marked busy, so it is the one released. */
69
+ private region;
70
+ /** Whether the loader is still waiting out its `delay`. */
71
+ private get waiting();
72
+ connectedCallback(): void;
73
+ disconnectedCallback(): void;
74
+ /** Marks the region `for` names busy, releasing one it named before. */
75
+ private mark;
76
+ private release;
77
+ protected updated(changed: PropertyValues<this>): void;
41
78
  protected willUpdate(changed: PropertyValues): void;
42
79
  protected render(): TemplateResult;
43
80
  }
@@ -4,27 +4,76 @@ 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, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, DelayController, elementById, HelixElement, hostAria, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { claimBusy, releaseBusy } from '../busy.js';
10
11
  let HelixHeadlessLoader = class HelixHeadlessLoader extends HelixElement {
11
12
  constructor() {
12
13
  super(...arguments);
14
+ this.delay = 0;
15
+ this.for = '';
13
16
  this.label = 'Loading';
17
+ this.due = new DelayController(this);
18
+ this.region = null;
19
+ }
20
+ get waiting() {
21
+ return this.delay > 0 && !this.due.elapsed;
22
+ }
23
+ connectedCallback() {
24
+ super.connectedCallback();
25
+ if (this.delay > 0 && !this.due.elapsed) {
26
+ this.due.start(this.delay);
27
+ }
28
+ requestAnimationFrame(() => this.mark());
29
+ }
30
+ disconnectedCallback() {
31
+ super.disconnectedCallback();
32
+ this.release();
33
+ }
34
+ mark() {
35
+ const region = this.isConnected && this.for !== '' ? elementById(this, this.for) : null;
36
+ if (region === this.region) {
37
+ return;
38
+ }
39
+ this.release();
40
+ if (region) {
41
+ claimBusy(region);
42
+ this.region = region;
43
+ }
44
+ }
45
+ release() {
46
+ if (this.region) {
47
+ releaseBusy(this.region);
48
+ this.region = null;
49
+ }
50
+ }
51
+ updated(changed) {
52
+ super.updated(changed);
53
+ if (changed.has('for') && changed.get('for') !== undefined) {
54
+ this.mark();
55
+ }
14
56
  }
15
57
  willUpdate(changed) {
16
58
  super.willUpdate(changed);
17
59
  hostAria(this.internals, { label: this.label, role: 'progressbar' });
18
60
  this.internals.ariaBusy = 'true';
61
+ this.internals.ariaHidden = this.waiting ? 'true' : null;
19
62
  }
20
63
  render() {
21
- return html `<svg part="svg" viewBox="0 0 24 24" aria-hidden="true">
64
+ return html `<svg part="svg" viewBox="0 0 24 24" aria-hidden="true" ?hidden=${this.waiting}>
22
65
  <circle part="track" cx="12" cy="12" r="10" fill="none" />
23
66
  <circle part="indicator" cx="12" cy="12" r="10" fill="none" />
24
67
  </svg>
25
- <span part="mark" aria-hidden="true"><slot></slot></span>`;
68
+ <span part="mark" aria-hidden="true" ?hidden=${this.waiting}><slot></slot></span>`;
26
69
  }
27
70
  };
71
+ __decorate([
72
+ property({ type: Number })
73
+ ], HelixHeadlessLoader.prototype, "delay", void 0);
74
+ __decorate([
75
+ property()
76
+ ], HelixHeadlessLoader.prototype, "for", void 0);
28
77
  __decorate([
29
78
  property()
30
79
  ], HelixHeadlessLoader.prototype, "label", void 0);
@@ -22,6 +22,8 @@ export type MeterBand = 'fair' | 'good' | 'poor';
22
22
  * @aliases Gauge, Level, Capacity
23
23
  * @documentation https://fusion.dev/helix/components/meter
24
24
  *
25
+ * @slot label - A visible label. Its words name the meter in place of `label`.
26
+ *
25
27
  * @csspart groove - The unfilled circle, in `ring` and `arc`.
26
28
  * @csspart indicator - The part of the track the value fills.
27
29
  * @csspart indicator-fair - The fill while the reading is in a band next to the good band. Set alongside `indicator`.
@@ -77,6 +79,9 @@ export declare class HelixHeadlessMeter extends HelixElement {
77
79
  high?: number;
78
80
  /**
79
81
  * Accessible name for the reading.
82
+ *
83
+ * A visible label in the `label` slot names it instead, with the words it
84
+ * shows, so the name and the label cannot disagree.
80
85
  */
81
86
  label: string;
82
87
  /**
@@ -218,6 +223,8 @@ export declare class HelixHeadlessMeter extends HelixElement {
218
223
  * where the arc starts.
219
224
  */
220
225
  private renderRound;
226
+ /** A label's words changed, so the name is written again. */
227
+ private readonly renamed;
221
228
  protected render(): TemplateResult;
222
229
  }
223
230
  declare global {
@@ -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 { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, slottedWords } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  const percents = new Map();
@@ -29,6 +29,9 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
29
29
  this.showValue = false;
30
30
  this.value = 0;
31
31
  this.valueText = '';
32
+ this.renamed = () => {
33
+ this.requestUpdate();
34
+ };
32
35
  }
33
36
  get span() {
34
37
  const min = Number.isFinite(this.min) ? this.min : 0;
@@ -82,7 +85,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
82
85
  willUpdate(changed) {
83
86
  super.willUpdate(changed);
84
87
  const { max, min } = this.span;
85
- hostAria(this.internals, { label: this.label, role: 'meter' });
88
+ hostAria(this.internals, { label: slottedWords(this, ['label']) || this.label, role: 'meter' });
86
89
  this.internals.ariaValueMin = String(min);
87
90
  this.internals.ariaValueMax = String(max);
88
91
  this.internals.ariaValueNow = String(this.reading);
@@ -121,7 +124,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
121
124
  const beyond = this.beyond;
122
125
  const { max, min } = this.span;
123
126
  const at = (value) => `${((value - min) / (max - min)) * 100}%`;
124
- return html `${this.shape
127
+ return html `<slot name="label" @slotchange=${this.renamed}></slot>${this.shape
125
128
  ? this.renderRound()
126
129
  : html `<div part="track">
127
130
  ${bounds
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Where a key moves focus in a feed of `count` rows, from the row at `at`, or
3
+ * `undefined` for a key the feed does not take.
4
+ *
5
+ * `onRow` says whether focus is on the row's own box. The arrows and `Home` and
6
+ * `End` move only from there, so a control inside a row keeps its keys.
7
+ * `PageDown` and `PageUp` move from anywhere in a row, which is how the APG feed
8
+ * pattern moves on from inside an article. The answer stays inside the feed.
9
+ */
10
+ export declare const feedStep: (key: string, at: number, count: number, onRow: boolean) => number | undefined;
@@ -0,0 +1,14 @@
1
+ export const feedStep = (key, at, count, onRow) => {
2
+ if (count <= 0) {
3
+ return undefined;
4
+ }
5
+ const anywhere = { PageDown: at + 1, PageUp: at - 1 };
6
+ const fromRow = {
7
+ ArrowDown: at + 1,
8
+ ArrowUp: at - 1,
9
+ End: count - 1,
10
+ Home: 0,
11
+ };
12
+ const to = anywhere[key] ?? (onRow ? fromRow[key] : undefined);
13
+ return to === undefined ? undefined : Math.max(0, Math.min(to, count - 1));
14
+ };
@@ -79,6 +79,12 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
79
79
  * Empty is the runtime's own.
80
80
  */
81
81
  locale: string;
82
+ /**
83
+ * @internal Set by the feed: this row's place in it, counting from one, read
84
+ * as `aria-posinset`. Zero leaves the place to the browser, which counts the
85
+ * rows it can see.
86
+ */
87
+ position: number;
82
88
  /**
83
89
  * Whether the notification has been read.
84
90
  *
@@ -96,6 +102,17 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
96
102
  */
97
103
  get read(): boolean;
98
104
  set read(next: boolean);
105
+ /**
106
+ * @internal Set by the feed: how many rows the feed holds in all, read as
107
+ * `aria-setsize`, and `-1` when nobody knows. Zero leaves it to the browser.
108
+ */
109
+ setSize: number;
110
+ /**
111
+ * @internal Set by the feed: whether `Tab` stops on this row. Every row is a
112
+ * stop until the feed has chosen one, which is also what a server sends, so
113
+ * the first render on the client is the server's.
114
+ */
115
+ stop: boolean;
99
116
  /**
100
117
  * What a screen reader hears before an unread notification's text.
101
118
  *