@fusion.dev/helix-headless 0.142.0 → 0.143.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 (217) hide show
  1. package/custom-elements.json +16710 -707
  2. package/dist/accordion/item/accordion-item.d.ts +38 -2
  3. package/dist/accordion/item/accordion-item.js +38 -6
  4. package/dist/accordion/root/accordion-root.d.ts +23 -1
  5. package/dist/accordion/root/accordion-root.js +39 -3
  6. package/dist/announcement/root/announcement-root.d.ts +11 -7
  7. package/dist/announcement/root/announcement-root.js +1 -1
  8. package/dist/app-header/root/app-header-root.d.ts +21 -0
  9. package/dist/app-header/root/app-header-root.js +31 -5
  10. package/dist/area-chart/root/area-chart-root.js +16 -5
  11. package/dist/avatar/group/avatar-group.d.ts +24 -3
  12. package/dist/avatar/group/avatar-group.js +34 -2
  13. package/dist/avatar/root/avatar-root.d.ts +3 -1
  14. package/dist/avatar/root/avatar-root.js +8 -1
  15. package/dist/badge/root/badge-root.d.ts +8 -0
  16. package/dist/bar-chart/root/bar-chart-root.d.ts +19 -3
  17. package/dist/bar-chart/root/bar-chart-root.js +43 -13
  18. package/dist/blockquote/root/blockquote-root.d.ts +12 -7
  19. package/dist/blockquote/root/blockquote-root.js +6 -4
  20. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +9 -3
  21. package/dist/breadcrumbs/root/breadcrumbs-root.js +9 -1
  22. package/dist/button/root/button-root.d.ts +37 -0
  23. package/dist/button/root/button-root.js +44 -0
  24. package/dist/callout/root/callout-root.d.ts +14 -5
  25. package/dist/callout/root/callout-root.js +11 -3
  26. package/dist/canvas/root/canvas-root.d.ts +1 -0
  27. package/dist/canvas/root/canvas-root.js +10 -1
  28. package/dist/carousel/root/carousel-root.d.ts +47 -5
  29. package/dist/carousel/root/carousel-root.js +53 -21
  30. package/dist/chat/composer/chat-composer.d.ts +13 -11
  31. package/dist/chat/composer/chat-composer.js +6 -12
  32. package/dist/chat/message/chat-message.js +8 -1
  33. package/dist/chat/root/chat-root.d.ts +1 -0
  34. package/dist/chat/root/chat-root.js +2 -1
  35. package/dist/checkbox/root/checkbox-root.d.ts +7 -0
  36. package/dist/checkbox/root/checkbox-root.js +7 -3
  37. package/dist/checkbox-group/root/checkbox-group-root.d.ts +65 -32
  38. package/dist/checkbox-group/root/checkbox-group-root.js +85 -37
  39. package/dist/code/root/code-root.d.ts +1 -1
  40. package/dist/code-block/root/code-block-root.d.ts +19 -0
  41. package/dist/code-block/root/code-block-root.js +21 -1
  42. package/dist/color-input/root/color-input-root.d.ts +26 -1
  43. package/dist/color-input/root/color-input-root.js +32 -3
  44. package/dist/color-picker/root/color-picker-root.d.ts +37 -7
  45. package/dist/color-picker/root/color-picker-root.js +83 -6
  46. package/dist/combobox/root/combobox-root.d.ts +90 -2
  47. package/dist/combobox/root/combobox-root.js +160 -19
  48. package/dist/command/root/command-root.d.ts +17 -6
  49. package/dist/command/root/command-root.js +12 -1
  50. package/dist/copy-button/root/copy-button-root.d.ts +21 -6
  51. package/dist/copy-button/root/copy-button-root.js +11 -1
  52. package/dist/data-table/root/data-table-root.d.ts +164 -10
  53. package/dist/data-table/root/data-table-root.js +488 -57
  54. package/dist/date-input/root/date-input-root.d.ts +53 -8
  55. package/dist/date-input/root/date-input-root.js +70 -18
  56. package/dist/date-picker/root/date-picker-root.d.ts +84 -17
  57. package/dist/date-picker/root/date-picker-root.js +125 -20
  58. package/dist/dialog/root/dialog-root.d.ts +38 -10
  59. package/dist/dialog/root/dialog-root.js +55 -2
  60. package/dist/donut-chart/root/donut-chart-root.d.ts +39 -15
  61. package/dist/donut-chart/root/donut-chart-root.js +30 -10
  62. package/dist/drawer/root/drawer-root.d.ts +32 -7
  63. package/dist/drawer/root/drawer-root.js +50 -5
  64. package/dist/drilldown/item/drilldown-item.d.ts +16 -13
  65. package/dist/drilldown/item/drilldown-item.js +6 -2
  66. package/dist/drilldown/root/drilldown-root.d.ts +21 -5
  67. package/dist/drilldown/root/drilldown-root.js +26 -10
  68. package/dist/element-events.generated.js +35 -9
  69. package/dist/file-input/root/file-input-root.d.ts +18 -1
  70. package/dist/file-input/root/file-input-root.js +34 -8
  71. package/dist/filter-input/menu/filter-menu.d.ts +9 -0
  72. package/dist/filter-input/menu/filter-menu.js +8 -0
  73. package/dist/filter-input/root/filter-input-root.d.ts +5 -2
  74. package/dist/filter-input/root/filter-input-root.js +22 -7
  75. package/dist/format-date/root/format-date-root.d.ts +11 -3
  76. package/dist/format-date/root/format-date-root.js +26 -5
  77. package/dist/graph/root/graph-root.d.ts +24 -3
  78. package/dist/graph/root/graph-root.js +66 -12
  79. package/dist/heatmap/root/buckets.d.ts +2 -1
  80. package/dist/heatmap/root/buckets.js +5 -1
  81. package/dist/heatmap/root/heatmap-root.d.ts +27 -6
  82. package/dist/heatmap/root/heatmap-root.js +23 -6
  83. package/dist/icon/root/icon-root.d.ts +24 -3
  84. package/dist/icon/root/icon-root.js +34 -1
  85. package/dist/icon-button/root/icon-button-root.d.ts +1 -1
  86. package/dist/image/root/image-root.d.ts +9 -2
  87. package/dist/image/root/image-root.js +8 -2
  88. package/dist/input/root/input-root.d.ts +51 -9
  89. package/dist/input/root/input-root.js +46 -5
  90. package/dist/kbd/root/kbd-root.d.ts +12 -4
  91. package/dist/kbd/root/kbd-root.js +10 -11
  92. package/dist/legend/root/legend-root.d.ts +7 -0
  93. package/dist/legend/root/legend-root.js +7 -2
  94. package/dist/lightbox/item/lightbox-item.d.ts +2 -1
  95. package/dist/lightbox/root/lightbox-root.d.ts +16 -5
  96. package/dist/lightbox/root/lightbox-root.js +24 -3
  97. package/dist/line-chart/root/line-chart-root.d.ts +19 -2
  98. package/dist/line-chart/root/line-chart-root.js +40 -13
  99. package/dist/list/item/list-item.d.ts +11 -5
  100. package/dist/list/item/list-item.js +5 -6
  101. package/dist/listbox/root/listbox-root.d.ts +111 -2
  102. package/dist/listbox/root/listbox-root.js +144 -16
  103. package/dist/map/root/map-root.d.ts +19 -4
  104. package/dist/map/root/map-root.js +36 -7
  105. package/dist/megamenu/root/megamenu-root.d.ts +10 -1
  106. package/dist/megamenu/root/megamenu-root.js +10 -1
  107. package/dist/menu/item/menu-item.d.ts +41 -12
  108. package/dist/menu/item/menu-item.js +46 -3
  109. package/dist/menu/root/menu-root.d.ts +32 -2
  110. package/dist/menu/root/menu-root.js +48 -8
  111. package/dist/meter/root/meter-root.d.ts +6 -0
  112. package/dist/meter/root/meter-root.js +18 -2
  113. package/dist/navigation/root/navigation-root.d.ts +10 -2
  114. package/dist/navigation/root/navigation-root.js +13 -2
  115. package/dist/notifications/group/notification-group.d.ts +1 -0
  116. package/dist/notifications/notification/notification.d.ts +12 -0
  117. package/dist/notifications/notification/notification.js +31 -2
  118. package/dist/notifications/root/notifications-root.d.ts +7 -0
  119. package/dist/notifications/root/notifications-root.js +17 -2
  120. package/dist/number-input/root/number-input-root.d.ts +62 -7
  121. package/dist/number-input/root/number-input-root.js +68 -16
  122. package/dist/object-inspector/root/object-inspector-root.d.ts +48 -5
  123. package/dist/object-inspector/root/object-inspector-root.js +91 -11
  124. package/dist/option/root/option-root.d.ts +4 -1
  125. package/dist/option/root/option-root.js +1 -0
  126. package/dist/outline/root/outline-root.d.ts +13 -2
  127. package/dist/outline/root/outline-root.js +4 -2
  128. package/dist/pagination/root/pagination-root.d.ts +19 -1
  129. package/dist/pagination/root/pagination-root.js +49 -9
  130. package/dist/pagination/size/pagination-size.d.ts +13 -3
  131. package/dist/pagination/size/pagination-size.js +7 -0
  132. package/dist/pin-input/root/pin-input-root.d.ts +16 -0
  133. package/dist/pin-input/root/pin-input-root.js +19 -5
  134. package/dist/popover/root/popover-root.d.ts +19 -2
  135. package/dist/popover/root/popover-root.js +22 -1
  136. package/dist/progress/root/progress-root.d.ts +16 -1
  137. package/dist/progress/root/progress-root.js +31 -7
  138. package/dist/qr-code/root/qr-code-root.d.ts +19 -0
  139. package/dist/qr-code/root/qr-code-root.js +29 -1
  140. package/dist/radio/group/radio-group.d.ts +41 -0
  141. package/dist/radio/group/radio-group.js +50 -14
  142. package/dist/radio/root/radio-root.d.ts +8 -5
  143. package/dist/radio/root/radio-root.js +1 -2
  144. package/dist/rating/root/rating-root.d.ts +24 -1
  145. package/dist/rating/root/rating-root.js +24 -7
  146. package/dist/scatter-chart/root/scatter-chart-root.js +15 -7
  147. package/dist/select/root/select-root.d.ts +69 -3
  148. package/dist/select/root/select-root.js +92 -8
  149. package/dist/skeleton/root/skeleton-root.d.ts +14 -3
  150. package/dist/skeleton/root/skeleton-root.js +17 -3
  151. package/dist/skip-link/root/skip-link-root.d.ts +10 -0
  152. package/dist/skip-link/root/skip-link-root.js +15 -1
  153. package/dist/slider/root/slider-root.d.ts +20 -2
  154. package/dist/slider/root/slider-root.js +41 -3
  155. package/dist/sparkline/root/sparkline-root.d.ts +9 -10
  156. package/dist/speed-dial/item/speed-dial-item.d.ts +11 -5
  157. package/dist/speed-dial/item/speed-dial-item.js +7 -2
  158. package/dist/speed-dial/root/speed-dial-root.d.ts +15 -4
  159. package/dist/speed-dial/root/speed-dial-root.js +19 -5
  160. package/dist/splitter/root/splitter-root.d.ts +28 -6
  161. package/dist/splitter/root/splitter-root.js +35 -1
  162. package/dist/stat/root/stat-root.d.ts +7 -1
  163. package/dist/stat/root/stat-root.js +14 -1
  164. package/dist/steps/root/steps-root.js +1 -1
  165. package/dist/switch/root/switch-root.d.ts +2 -0
  166. package/dist/switch/root/switch-root.js +1 -0
  167. package/dist/table/cell/table-cell.d.ts +24 -4
  168. package/dist/table/cell/table-cell.js +21 -1
  169. package/dist/table/column/table-column.d.ts +32 -6
  170. package/dist/table/column/table-column.js +36 -6
  171. package/dist/table/root/table-root.d.ts +94 -3
  172. package/dist/table/root/table-root.js +128 -5
  173. package/dist/table/row/table-row.d.ts +7 -0
  174. package/dist/table/row/table-row.js +9 -1
  175. package/dist/tabs/root/tabs-root.d.ts +6 -1
  176. package/dist/tabs/root/tabs-root.js +6 -2
  177. package/dist/tag/root/tag-root.d.ts +23 -1
  178. package/dist/tag/root/tag-root.js +16 -2
  179. package/dist/textarea/root/textarea-root.d.ts +4 -0
  180. package/dist/textarea/root/textarea-root.js +1 -1
  181. package/dist/time-input/root/time-input-root.d.ts +28 -2
  182. package/dist/time-input/root/time-input-root.js +46 -10
  183. package/dist/time-picker/root/time-picker-root.d.ts +101 -18
  184. package/dist/time-picker/root/time-picker-root.js +115 -16
  185. package/dist/toast/root/toast-root.d.ts +64 -1
  186. package/dist/toast/root/toast-root.js +147 -2
  187. package/dist/toggle/group/toggle-group.d.ts +38 -1
  188. package/dist/toggle/group/toggle-group.js +34 -8
  189. package/dist/toggle/root/toggle-root.d.ts +14 -10
  190. package/dist/toggle/root/toggle-root.js +3 -3
  191. package/dist/toolbar/root/toolbar-root.d.ts +55 -15
  192. package/dist/toolbar/root/toolbar-root.js +99 -16
  193. package/dist/tooltip/root/tooltip-root.d.ts +37 -4
  194. package/dist/tooltip/root/tooltip-root.js +48 -0
  195. package/dist/tour/root/tour-root.d.ts +34 -5
  196. package/dist/tour/root/tour-root.js +51 -14
  197. package/dist/tour/stop/tour-stop.d.ts +18 -1
  198. package/dist/tour/stop/tour-stop.js +25 -5
  199. package/dist/tree/item/tree-item.d.ts +21 -5
  200. package/dist/tree/item/tree-item.js +14 -4
  201. package/dist/tree/root/tree-root.d.ts +29 -5
  202. package/dist/tree/root/tree-root.js +64 -8
  203. package/dist/trend/root/trend-root.d.ts +23 -1
  204. package/dist/trend/root/trend-root.js +28 -7
  205. package/dist/video/root/keys.d.ts +5 -0
  206. package/dist/video/root/keys.js +13 -1
  207. package/dist/video/root/video-root.d.ts +19 -0
  208. package/dist/video/root/video-root.js +48 -4
  209. package/dist/video-playlist/menu/video-playlist-menu.d.ts +20 -2
  210. package/dist/video-playlist/menu/video-playlist-menu.js +27 -5
  211. package/dist/video-playlist/root/video-playlist-root.js +6 -3
  212. package/dist/virtual-list/root/virtual-list-root.d.ts +52 -4
  213. package/dist/virtual-list/root/virtual-list-root.js +103 -21
  214. package/lint.json +1 -1
  215. package/package.json +5 -5
  216. package/types/jsx.d.ts +4348 -965
  217. package/types/vue.d.ts +972 -232
@@ -11,39 +11,71 @@ import { HelixHeadlessRadio } from '../root/radio-root.js';
11
11
  let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.focusable = true;
14
15
  this.keys = new KeyboardNavigationController(this, { items: () => this.items });
15
16
  this.cascade = new DisabledCascadeController(this, {
16
17
  items: () => this.items,
17
18
  });
18
19
  this.handleSelection = (event) => {
19
20
  event.stopPropagation();
20
- if (event.defaultPrevented) {
21
- return;
22
- }
23
- const next = event.detail.value;
24
- if (next === this.value) {
25
- return;
26
- }
27
- this.requestValue(next);
21
+ queueMicrotask(() => {
22
+ if (event.defaultPrevented) {
23
+ return;
24
+ }
25
+ const next = event.detail.value;
26
+ if (next === this.value) {
27
+ return;
28
+ }
29
+ this.requestValue(next);
30
+ });
28
31
  };
29
32
  }
30
33
  get items() {
31
- return [...this.children].filter((child) => child instanceof HelixHeadlessRadio);
34
+ return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessRadio);
35
+ }
36
+ get answer() {
37
+ return this.items.find((item) => item.value === this.value);
38
+ }
39
+ get answered() {
40
+ if (this.value === '') {
41
+ return false;
42
+ }
43
+ const items = this.items;
44
+ return items.length === 0 || items.some((item) => item.value === this.value);
45
+ }
46
+ adopt() {
47
+ if (this.value !== '' || this.children === undefined) {
48
+ return;
49
+ }
50
+ const declared = this.items.find((item) => item.defaultSelected);
51
+ if (declared) {
52
+ this.defaultValue = declared.value;
53
+ }
32
54
  }
33
55
  place() {
34
56
  const items = this.items;
35
- const answer = items.find((item) => item.value === this.value);
57
+ const answer = this.answer;
36
58
  for (const item of items) {
37
59
  item.selected = item === answer;
38
60
  }
39
- const entry = answer ? undefined : items.find((item) => !item.disabled);
61
+ const entry = this.focusable ? this.entry() : undefined;
40
62
  for (const item of items) {
41
63
  item.focusable = item === entry;
42
64
  }
43
65
  }
66
+ entry() {
67
+ const answer = this.answer;
68
+ return answer && !answer.disabled ? answer : this.items.find((item) => !item.disabled);
69
+ }
70
+ get focusTarget() {
71
+ return (super.focusTarget ??
72
+ this.entry()?.shadowRoot?.querySelector('[part="item"]') ??
73
+ null);
74
+ }
44
75
  sync() {
45
- this.internals.setFormValue(this.value === '' ? null : this.value);
46
- this.reportRequired(this.value === '', 'Choose one to continue.', 'items');
76
+ const answered = this.answered;
77
+ this.internals.setFormValue(answered ? this.value : null);
78
+ this.reportRequired(!answered, 'Choose one to continue.', 'items');
47
79
  }
48
80
  connectedCallback() {
49
81
  super.connectedCallback();
@@ -55,6 +87,7 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
55
87
  }
56
88
  willUpdate(changed) {
57
89
  super.willUpdate(changed);
90
+ this.adopt();
58
91
  hostAria(this.internals, { label: this.label, role: 'radiogroup' });
59
92
  this.internals.ariaRequired = this.required ? 'true' : null;
60
93
  }
@@ -67,11 +100,14 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
67
100
  render() {
68
101
  return html `${this.renderGroupLabel()}
69
102
  <div part="items">
70
- <slot @slotchange=${() => this.place()}></slot>
103
+ <slot @slotchange=${() => this.requestUpdate()}></slot>
71
104
  </div>
72
105
  ${this.renderHint()}${this.renderFeedback()}`;
73
106
  }
74
107
  };
108
+ __decorate([
109
+ property({ attribute: false })
110
+ ], HelixHeadlessRadioGroup.prototype, "focusable", void 0);
75
111
  __decorate([
76
112
  property({ reflect: true })
77
113
  ], HelixHeadlessRadioGroup.prototype, "orientation", void 0);
@@ -73,11 +73,14 @@ export declare class HelixHeadlessRadio extends HelixHeadlessRadio_base {
73
73
  get defaultSelected(): boolean;
74
74
  set defaultSelected(next: boolean);
75
75
  /**
76
- * Whether the radio holds the group's tab stop while nothing is chosen.
76
+ * Whether the radio holds the group's tab stop. Unset, the chosen radio
77
+ * holds it.
77
78
  *
78
79
  * The stop rides on the selection, and an unanswered group has no selection,
79
80
  * so without this every choice is `tabindex="-1"` and the keyboard cannot
80
- * enter the set at all.
81
+ * enter the set at all. The group writes `true` for the chosen radio, or for
82
+ * the first that can be chosen while nothing is, and `false` for every radio,
83
+ * the chosen one too, while a toolbar the group sits in holds the stop.
81
84
  *
82
85
  * **Written by the group, with no attribute, and it reaches the control
83
86
  * rather than the host.** A `tabindex` on the host is two faults at once: it
@@ -88,15 +91,15 @@ export declare class HelixHeadlessRadio extends HelixHeadlessRadio_base {
88
91
  * them an unchecked radio. `Toggle.Root` carries the same property for the
89
92
  * same reason.
90
93
  */
91
- focusable: boolean;
94
+ focusable?: boolean;
92
95
  /**
93
96
  * Asks for the render that follows a hydration, so the stop stops being every
94
97
  * choice's.
95
98
  *
96
99
  * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
97
100
  * false, which is what keeps the hydrating render the server's. Nothing else
98
- * would ask: the group writes an already-false `focusable` onto every choice
99
- * but one, which changes nothing and so re-renders nothing.
101
+ * is sure to ask: a write from the group that leaves `focusable` as it was
102
+ * re-renders nothing.
100
103
  */
101
104
  protected firstUpdated(changed: PropertyValues): void;
102
105
  protected render(): TemplateResult;
@@ -11,7 +11,6 @@ let HelixHeadlessRadio = class HelixHeadlessRadio extends MixinSelectableItem(He
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.clicks = new HostClickController(this, () => this.partNamed('item'));
14
- this.focusable = false;
15
14
  }
16
15
  get defaultSelected() {
17
16
  return this.hasAttribute('selected');
@@ -30,7 +29,7 @@ let HelixHeadlessRadio = class HelixHeadlessRadio extends MixinSelectableItem(He
30
29
  role="radio"
31
30
  aria-checked=${this.selected ? 'true' : 'false'}
32
31
  ?disabled=${this.disabled}
33
- tabindex=${this.selected || this.focusable || !this.childrenKnown ? '0' : '-1'}
32
+ tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
34
33
  @click=${() => this.select()}
35
34
  >
36
35
  <span part="box"></span>
@@ -1,4 +1,4 @@
1
- import { HelixFormElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
4
4
  /**
@@ -21,6 +21,11 @@ declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion
21
21
  * @aliases Stars, Score, Review score
22
22
  * @documentation https://fusion.dev/helix/components/rating
23
23
  *
24
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
25
+ * @slot hint - Help under the control, for content richer than the `hint` text.
26
+ * @slot label - The label, for content richer than the `label` text.
27
+ * @slot mark - The shape of a mark, copied into every position.
28
+ *
24
29
  * @event {Event} change - The score was chosen. Sent together with `input`, because each choice commits the score.
25
30
  * @event {HelixBeforeChangeEvent} helix-before-change - The score is about to change. Cancelable: cancel it to keep the current score.
26
31
  * @event {Event} input - The score changed.
@@ -92,6 +97,14 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
92
97
  * nonsense is the fault this pays off, so neither case is quietly ignored.
93
98
  */
94
99
  min: number;
100
+ /**
101
+ * The name of each mark a reader can choose, with `{score}` and `{max}`
102
+ * filled in. Set it to translate "3 out of 5".
103
+ *
104
+ * Also what a read-only rating says for its score where `value-text` is
105
+ * empty.
106
+ */
107
+ optionLabel: string;
95
108
  /**
96
109
  * Shows the score without letting users change it.
97
110
  *
@@ -128,6 +141,7 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
128
141
  * and each of its options by the score that option is.
129
142
  */
130
143
  valueText: string;
144
+ protected readonly aria: AriaForwardController;
131
145
  /**
132
146
  * The shape a consumer slotted, if they slotted one.
133
147
  *
@@ -255,6 +269,15 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
255
269
  protected updated(changed: PropertyValues): void;
256
270
  protected willUpdate(changed: PropertyValues): void;
257
271
  protected firstUpdated(changed: PropertyValues): void;
272
+ /** The name of one score, as `optionLabel` writes it. */
273
+ private nameFor;
274
+ /**
275
+ * The option holding the tab stop: the chosen one, or for a score between
276
+ * steps, such as an average of 3.5 on whole marks, the step below it, and
277
+ * the first while nothing is chosen. Between steps no option matched, and
278
+ * the rating left the tab order.
279
+ */
280
+ private stopAt;
258
281
  protected render(): TemplateResult;
259
282
  /**
260
283
  * The value as a number, or `NaN` while the control is empty.
@@ -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 { valueAsNumber as asNumber, customElement, numberAsValue as fromNumber, HelixFormElement, KeyboardNavigationController, MixinControllerValue, MixinField, reportRuntime, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, valueAsNumber as asNumber, customElement, fillTemplate, numberAsValue as fromNumber, HelixFormElement, KeyboardNavigationController, MixinControllerValue, MixinField, reportRuntime, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue(MixinField(HelixFormElement)) {
@@ -13,10 +13,14 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
13
13
  this.announcesInput = false;
14
14
  this.max = 5;
15
15
  this.min = 0;
16
+ this.optionLabel = '{score} out of {max}';
16
17
  this.readOnly = false;
17
18
  this.showValue = false;
18
19
  this.step = 1;
19
20
  this.valueText = '';
21
+ this.aria = new AriaForwardController(this, {
22
+ target: () => this.partNamed('marks'),
23
+ });
20
24
  this.markSource = undefined;
21
25
  this.markRead = false;
22
26
  this.previewing = undefined;
@@ -190,11 +194,23 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
190
194
  this.finishHydration();
191
195
  this.sync();
192
196
  }
197
+ nameFor(score) {
198
+ return fillTemplate(this.optionLabel, { max: this.max, score });
199
+ }
200
+ stopAt(answered, first) {
201
+ if (!answered) {
202
+ return first;
203
+ }
204
+ const below = this.steps.filter((score) => score <= this.score);
205
+ return below.at(-1) ?? first;
206
+ }
193
207
  render() {
194
208
  const showing = this.previewing ?? this.score;
195
- const said = this.valueText || `${this.score} out of ${this.max}`;
209
+ const said = this.valueText || this.nameFor(this.score);
196
210
  const count = Math.max(0, Math.round(this.max));
197
211
  const first = this.steps[0];
212
+ const answered = this.value !== '' && Number.isFinite(Number.parseFloat(this.value));
213
+ const stop = this.stopAt(answered, first);
198
214
  const mark = (layer, index) => html `<span part="mark">${this.markFor(layer, index)}</span>`;
199
215
  const marksIn = (layer) => Array.from({ length: count }, (_, index) => mark(layer, index));
200
216
  return html `${this.renderGroupLabel()}
@@ -240,14 +256,12 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
240
256
  part="option"
241
257
  role="radio"
242
258
  type="button"
243
- aria-checked=${this.score === score ? 'true' : 'false'}
244
- aria-label=${`${score} out of ${this.max}`}
259
+ aria-checked=${answered && this.score === score ? 'true' : 'false'}
260
+ aria-label=${this.nameFor(score)}
245
261
  data-score=${String(score)}
246
262
  style=${`--rating-step: ${index + 1}`}
247
263
  ?disabled=${this.isDisabled}
248
- tabindex=${this.score === score || (this.score === this.floor && score === first)
249
- ? '0'
250
- : '-1'}
264
+ tabindex=${score === stop ? '0' : '-1'}
251
265
  @click=${this.handleOptionClick}
252
266
  @pointerenter=${this.handleOptionEnter}
253
267
  ></button>`)}
@@ -275,6 +289,9 @@ __decorate([
275
289
  __decorate([
276
290
  property({ type: Number })
277
291
  ], HelixHeadlessRating.prototype, "min", void 0);
292
+ __decorate([
293
+ property({ attribute: 'option-label' })
294
+ ], HelixHeadlessRating.prototype, "optionLabel", void 0);
278
295
  __decorate([
279
296
  property({ attribute: 'readonly', reflect: true, type: Boolean })
280
297
  ], HelixHeadlessRating.prototype, "readOnly", void 0);
@@ -9,11 +9,19 @@ import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
11
11
  const REACH = 24;
12
- const coefficient = new Intl.NumberFormat(undefined, {
13
- maximumFractionDigits: 2,
14
- minimumFractionDigits: 2,
15
- signDisplay: 'exceptZero',
16
- });
12
+ const COEFFICIENTS = new Map();
13
+ const coefficientIn = (locale) => {
14
+ let found = COEFFICIENTS.get(locale);
15
+ if (!found) {
16
+ found = new Intl.NumberFormat(locale || undefined, {
17
+ maximumFractionDigits: 2,
18
+ minimumFractionDigits: 2,
19
+ signDisplay: 'exceptZero',
20
+ });
21
+ COEFFICIENTS.set(locale, found);
22
+ }
23
+ return found;
24
+ };
17
25
  const nice = ([low, high]) => {
18
26
  if (low === high) {
19
27
  return [low - 1, high + 1];
@@ -130,7 +138,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
130
138
  <div id=${CHART_PLOT} part="plot"></div>
131
139
  </div>`;
132
140
  }
133
- const format = formatNumbers('');
141
+ const format = formatNumbers(this.locale);
134
142
  const along = linear(field.x, [0, PLOT_WIDTH]);
135
143
  const up = linear(field.y, [PLOT_HEIGHT, 0]);
136
144
  return html `<div dir="ltr" part="plane">
@@ -177,7 +185,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
177
185
  const clauses = field.yNames.map((name, depth) => {
178
186
  const pairs = field.pairs[depth] ?? [];
179
187
  const r = correlation(pairs);
180
- const strength = r === undefined ? '' : `, ${this.correlationLabel} ${coefficient.format(r)}`;
188
+ const strength = r === undefined ? '' : `, ${this.correlationLabel} ${coefficientIn(this.locale).format(r)}`;
181
189
  return `${name} / ${field.xName}: ${pairs.length} ${this.pointsLabel}${strength}`;
182
190
  });
183
191
  const named = this.named ? `${this.named}. ` : '';
@@ -17,6 +17,14 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
17
17
  * `Select` owns the value, passes it down to the listbox, and takes changes
18
18
  * back, so the value lives in one place.
19
19
  *
20
+ * In the open list the arrow keys, Home, End, PageUp, PageDown and typed
21
+ * letters move the highlight, and Enter, Space or a click chooses it and
22
+ * closes the list. Tab closes the list and moves on. On the closed trigger,
23
+ * ArrowDown, Enter and Space open the list on the chosen option, ArrowUp opens
24
+ * it on the chosen or last option, Home and End open it on the first or last,
25
+ * and typed letters choose the option whose words start with them, as a native
26
+ * `<select>` does.
27
+ *
20
28
  * @since 0.1.0
21
29
  * @status stable
22
30
  * @category forms
@@ -25,6 +33,7 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
25
33
  *
26
34
  * @slot - The options, and any groups of them.
27
35
  * @slot end - Content after the chosen value, such as a count, a unit, or a clear button.
36
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
28
37
  * @slot hint - The hint, for content richer than plain text.
29
38
  * @slot indicator - The icon on the trigger that shows it opens, usually a caret.
30
39
  * @slot label - The label, for content richer than plain text.
@@ -34,8 +43,9 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
34
43
  * @event {HelixBeforeChangeEvent} helix-before-change - Relayed from the listbox when the value is about to change. Cancelable: canceling it stops the change and leaves the decision to you.
35
44
  * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the list closes. Cancelable: canceling it keeps the list open and leaves the decision to you.
36
45
  * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the list opens. Cancelable: canceling it keeps the list closed and leaves the decision to you.
46
+ * @event {HelixFilterEvent} helix-filter - Fired as the user types in the filter field of a `filterable` select, with the text the list is filtered by. Cancelable: cancel it to leave the list unfiltered and filter the options yourself.
37
47
  * @event {Event} input - Fired after the value changes.
38
- * @event {Event} toggle - Fired after the list opens or closes, with `newState` and `oldState`.
48
+ * @event {ToggleEvent} toggle - Fired after the list opens or closes, with `newState` and `oldState`.
39
49
  *
40
50
  * @csspart feedback - The message that says what is wrong with the value.
41
51
  * @csspart hint - Help under the trigger.
@@ -158,10 +168,64 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
158
168
  * the commit creates the text node it needs.
159
169
  */
160
170
  private get display();
171
+ /** Where the highlight goes when the list opens, set by the key that opened it. */
172
+ private opening;
173
+ /**
174
+ * The values of the options seen on the last update, so an option that
175
+ * leaves can be told apart from one that has not arrived yet.
176
+ */
177
+ private seen;
178
+ /**
179
+ * Hears an option's words change, which no slot reports, and the host's
180
+ * `aria-labelledby` change, which `triggerNames` reads.
181
+ *
182
+ * The trigger shows the chosen option's words, read at render time. A slot
183
+ * reports an option arriving or leaving and says nothing when the text of
184
+ * one already there changes, so a relabeled option left the old words on the
185
+ * trigger. `label` and `value` are attributes that change the words too.
186
+ *
187
+ * Made on connection, there being no `MutationObserver` on a server.
188
+ */
189
+ private watchers;
190
+ connectedCallback(): void;
191
+ disconnectedCallback(): void;
192
+ /** Opens the list with the highlight where the opening key says. */
193
+ private openAt;
161
194
  /** Closes the list and puts focus back where it came from. */
162
195
  private close;
163
196
  private readonly handleChange;
197
+ /**
198
+ * The keys of the closed trigger.
199
+ *
200
+ * Letters choose without opening, the way a native `<select>` and React
201
+ * Aria's Select do, so a reader who knows the answer types it. A Space in the
202
+ * middle of those letters is one of them. With `multiple`, a letter opens the
203
+ * list on the match instead, because adding to a set is a choice the reader
204
+ * makes in the list.
205
+ */
164
206
  private readonly handleTriggerKeydown;
207
+ /**
208
+ * Tab closes the list and lets focus move on, as a native `<select>` does.
209
+ *
210
+ * Measured before this: Tab moved focus to the next control and left the
211
+ * panel open over the page, because the panel closes on a press outside or
212
+ * on Escape and nothing closed it when focus left.
213
+ */
214
+ private readonly handleListKeydown;
215
+ /**
216
+ * Takes `<helix-option selected>` as the default value, and an option that
217
+ * has left out of the value.
218
+ *
219
+ * The first is what a native `<select>` does with `<option selected>`, and
220
+ * what `Option.defaultSelected` says: a form reset returns to it. A
221
+ * `default-value` on the select wins.
222
+ *
223
+ * The second keeps the trigger and the form in agreement. Measured before
224
+ * this: removing the chosen option made the trigger say "Choose" while the
225
+ * form still submitted the removed value. Only an option seen before is
226
+ * dropped, so a value set ahead of options that load later is kept.
227
+ */
228
+ private reconcileOptions;
165
229
  /** Reports the answer and the verdict on it. */
166
230
  private sync;
167
231
  protected willUpdate(changed: PropertyValues): void;
@@ -198,8 +262,10 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
198
262
  * native `<select>` says both, and this names the trigger by both, the label
199
263
  * and then the value, in that order.
200
264
  *
201
- * A consumer's own `aria-labelledby` on the host wins, carried to the trigger
202
- * by `AriaForwardController`, so nothing is written here to fight it.
265
+ * A consumer's own `aria-labelledby` on the host wins, and so does a
266
+ * `<label>` that names the host where there is no `label`. Both are carried
267
+ * to the trigger by `AriaForwardController`, which reads the value after
268
+ * them, so nothing is written here to fight it.
203
269
  */
204
270
  private get triggerNames();
205
271
  protected render(): TemplateResult;
@@ -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, HelixFormElement, hostAria, MixinControllerOpen, MixinControllerValue, MixinField, PopoverController, reportValues, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixFormElement, hostAria, MixinControllerOpen, MixinControllerValue, MixinField, PopoverController, reportValues, typesCharacter, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -16,6 +16,10 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
16
16
  constructor() {
17
17
  super(...arguments);
18
18
  this.aria = new AriaForwardController(this, {
19
+ labelledByAfter: () => {
20
+ const value = this.partNamed('value');
21
+ return value ? [value] : [];
22
+ },
19
23
  target: () => this.partNamed('trigger'),
20
24
  });
21
25
  this.emptyText = 'No matches';
@@ -36,6 +40,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
36
40
  placement: 'bottom-start',
37
41
  surface: () => this.partNamed('surface'),
38
42
  });
43
+ this.opening = 'selected';
44
+ this.seen = new Set();
45
+ this.watchers = [];
39
46
  this.handleChange = (event) => {
40
47
  const next = event.detail.value;
41
48
  if (!this.requestValue(next)) {
@@ -48,9 +55,39 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
48
55
  event.stopPropagation();
49
56
  };
50
57
  this.handleTriggerKeydown = (event) => {
51
- if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
58
+ const listbox = this.listbox;
59
+ if (typesCharacter(event) && (event.key !== ' ' || listbox?.typing)) {
52
60
  event.preventDefault();
53
- this.requestOpen(true);
61
+ const match = listbox?.typeaheadTo(event.key);
62
+ if (match === undefined) {
63
+ return;
64
+ }
65
+ if (this.multiple) {
66
+ this.opening = 'selected';
67
+ this.requestOpen(true);
68
+ }
69
+ else {
70
+ this.requestValue(match);
71
+ }
72
+ return;
73
+ }
74
+ const opens = {
75
+ ' ': 'selected',
76
+ ArrowDown: 'selected',
77
+ ArrowUp: 'last',
78
+ End: 'last',
79
+ Enter: 'selected',
80
+ Home: 'first',
81
+ };
82
+ const where = opens[event.key];
83
+ if (where !== undefined) {
84
+ event.preventDefault();
85
+ this.openAt(where === 'last' && event.key === 'ArrowUp' && this.value !== '' ? 'selected' : where);
86
+ }
87
+ };
88
+ this.handleListKeydown = (event) => {
89
+ if (event.key === 'Tab' && this.open) {
90
+ this.requestOpen(false);
54
91
  }
55
92
  };
56
93
  }
@@ -58,10 +95,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
58
95
  return this.listbox?.selectedIndex ?? -1;
59
96
  }
60
97
  set selectedIndex(at) {
61
- const listbox = this.listbox;
62
- if (listbox) {
63
- listbox.selectedIndex = at;
64
- }
98
+ this.value = this.listbox?.options[at]?.value ?? '';
65
99
  }
66
100
  get type() {
67
101
  return this.multiple ? 'select-multiple' : 'select-one';
@@ -79,6 +113,30 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
79
113
  }
80
114
  return chosen.size > 0 && !this.childrenKnown ? nothing : this.placeholder;
81
115
  }
116
+ connectedCallback() {
117
+ super.connectedCallback();
118
+ const words = new MutationObserver(() => this.requestUpdate());
119
+ words.observe(this, {
120
+ attributeFilter: ['label', 'value'],
121
+ characterData: true,
122
+ childList: true,
123
+ subtree: true,
124
+ });
125
+ const naming = new MutationObserver(() => this.requestUpdate());
126
+ naming.observe(this, { attributeFilter: ['aria-labelledby'] });
127
+ this.watchers = [words, naming];
128
+ }
129
+ disconnectedCallback() {
130
+ super.disconnectedCallback();
131
+ for (const watcher of this.watchers) {
132
+ watcher.disconnect();
133
+ }
134
+ this.watchers = [];
135
+ }
136
+ openAt(where) {
137
+ this.opening = where;
138
+ this.requestOpen(true);
139
+ }
82
140
  close() {
83
141
  if (!this.open) {
84
142
  return;
@@ -86,6 +144,24 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
86
144
  this.requestOpen(false);
87
145
  this.partNamed('trigger')?.focus();
88
146
  }
147
+ reconcileOptions() {
148
+ const options = optionsAmong(this.knownChildren);
149
+ if (this.defaultValue === '' && !this.hasAttribute('default-value')) {
150
+ const declared = options
151
+ .filter((option) => option.defaultSelected)
152
+ .map((option) => option.value);
153
+ if (declared.length > 0) {
154
+ this.defaultValue = this.multiple ? declared.join(',') : (declared.at(-1) ?? '');
155
+ }
156
+ }
157
+ const present = new Set(options.map((option) => option.value));
158
+ const held = this.value === '' ? [] : this.value.split(',');
159
+ const gone = held.filter((value) => this.seen.has(value) && !present.has(value));
160
+ this.seen = present;
161
+ if (gone.length > 0) {
162
+ this.value = held.filter((value) => !gone.includes(value)).join(',');
163
+ }
164
+ }
89
165
  sync() {
90
166
  if (this.multiple) {
91
167
  reportValues(this.internals, this.name, this.value === '' ? [] : this.value.split(','));
@@ -98,6 +174,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
98
174
  willUpdate(changed) {
99
175
  super.willUpdate(changed);
100
176
  hostAria(this.internals, { label: this.label });
177
+ if (this.childrenKnown) {
178
+ this.reconcileOptions();
179
+ }
101
180
  }
102
181
  firstUpdated(changed) {
103
182
  super.firstUpdated(changed);
@@ -109,6 +188,8 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
109
188
  this.linkHost();
110
189
  if (this.open) {
111
190
  if (changed.has('open')) {
191
+ this.listbox?.highlight(this.opening);
192
+ this.opening = 'selected';
112
193
  this.listbox?.focus();
113
194
  }
114
195
  }
@@ -129,7 +210,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
129
210
  .value=${this.value}
130
211
  ?multiple=${this.multiple}
131
212
  ?filterable=${this.filterable}
213
+ manual
132
214
  aria-label=${this.label || nothing}
215
+ @keydown=${this.handleListKeydown}
133
216
  @helix-before-change=${this.handleChange}
134
217
  @change=${this.stopInner}
135
218
  @input=${this.stopInner}
@@ -147,7 +230,8 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
147
230
  </slot>`;
148
231
  }
149
232
  get triggerNames() {
150
- if (this.hasAttribute('aria-labelledby')) {
233
+ const outside = (this.internals.labels?.length ?? 0) > 0 && !this.labeled;
234
+ if (this.hasAttribute('aria-labelledby') || outside) {
151
235
  return nothing;
152
236
  }
153
237
  return this.labeled ? `${this.labelId} value` : 'value';
@@ -4,9 +4,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
4
4
  * @summary A placeholder shown in place of content that is still loading.
5
5
  *
6
6
  * @description
7
- * Sets `aria-busy` and a polite live region, so a screen reader announces that
8
- * the region is loading. Both are set through `ElementInternals`, which writes
9
- * no attributes.
7
+ * A skeleton is hidden from screen readers unless it has a `label`. An empty
8
+ * placeholder has nothing to say, and a page of them would be a page of
9
+ * unnamed shapes. Tell a screen reader that a region is loading on the region
10
+ * itself: set `aria-busy="true"` on it while it loads, and put a `Loader` or a
11
+ * status message in it. Set `label` on one skeleton to have it read out by
12
+ * name, such as "Loading profile", when a reader reaches it. The role and the
13
+ * name are set through `ElementInternals`, which writes no attributes.
10
14
  *
11
15
  * There is no `shape` property. Whether a placeholder is a line, a block, or a
12
16
  * circle is presentation, which belongs to the styled tier.
@@ -20,6 +24,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
20
24
  * @csspart skeleton - The placeholder shape.
21
25
  */
22
26
  export declare class HelixHeadlessSkeleton extends HelixElement {
27
+ /**
28
+ * What is loading, read out as the placeholder's name, such as "Loading
29
+ * profile".
30
+ *
31
+ * Absent hides the placeholder from screen readers.
32
+ */
33
+ label: string;
23
34
  protected willUpdate(changed: PropertyValues): void;
24
35
  protected render(): TemplateResult;
25
36
  }