@fusion.dev/helix-headless 0.142.1 → 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
@@ -1,4 +1,4 @@
1
- import { HelixElement, KeyboardNavigationController, type Placement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement, KeyboardNavigationController, type Placement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
4
4
  /**
@@ -9,6 +9,12 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
9
9
  * menu renders its surface in the top layer with the popover API, so an
10
10
  * ancestor with `overflow: hidden` does not clip it and it needs no `z-index`.
11
11
  *
12
+ * Slot a `helix-button` or `helix-icon-button` as the trigger, and the menu
13
+ * tells it that it opens a menu and whether the menu is open. A plain
14
+ * `<button>` can only be told by writing attributes onto your markup, which
15
+ * the menu does not do, so give it `aria-haspopup="menu"` and set its
16
+ * `aria-expanded` from the `toggle` event.
17
+ *
12
18
  * Submenus drill down. A submenu replaces the panel's content, and a back row
13
19
  * returns to the previous page. One panel avoids the diagonal pointer path that
14
20
  * side-by-side panels need, stays inside a narrow window, and works on touch
@@ -30,7 +36,7 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
30
36
  * @event {HelixBeforeCloseEvent} helix-before-close - The menu is about to close. Cancelable: cancel it to keep it open.
31
37
  * @event {HelixBeforeOpenEvent} helix-before-open - The menu is about to open. Cancelable: cancel it to keep it closed.
32
38
  * @event {HelixSelectEvent} helix-select - An item was chosen. Cancelable: cancel it to keep the menu open.
33
- * @event {Event} toggle - The menu opened or closed. The event carries `newState` and `oldState`.
39
+ * @event {ToggleEvent} toggle - The menu opened or closed. The event carries `newState` and `oldState`.
34
40
  *
35
41
  * @csspart back - The control that returns from a submenu.
36
42
  * @csspart back-mark - The glyph on the back control of a submenu.
@@ -53,6 +59,10 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
53
59
  backLabel: string;
54
60
  /**
55
61
  * Accessible name for the surface.
62
+ *
63
+ * Empty by default, which names the surface by its trigger, so a menu opened
64
+ * from "Actions" is announced as "Actions". An `aria-label` or
65
+ * `aria-labelledby` on the host names it too, and wins over both.
56
66
  */
57
67
  label: string;
58
68
  /**
@@ -92,6 +102,8 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
92
102
  * menu that drills, so those keys are spoken for here rather than unhandled.
93
103
  */
94
104
  protected readonly keys: KeyboardNavigationController;
105
+ /** Carries a host `aria-label` and `aria-labelledby` to the surface. */
106
+ protected readonly aria: AriaForwardController;
95
107
  private readonly panel;
96
108
  /**
97
109
  * Tells a slotted trigger the disclosure state, by property.
@@ -108,6 +120,8 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
108
120
  private get items();
109
121
  /** Opens the menu. */
110
122
  show(): void;
123
+ /** Opens the menu with focus on its first or its last row. */
124
+ private openAt;
111
125
  /** Closes the menu and returns it to its first page. */
112
126
  hide(): void;
113
127
  /** Shows an item's submenu in place of the current page. */
@@ -124,10 +138,26 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
124
138
  */
125
139
  private readonly handleFocusIn;
126
140
  private readonly handleKeydown;
141
+ /**
142
+ * Down and Up on the trigger open the menu on its first and its last row.
143
+ *
144
+ * Everything else passes through. Escape in particular is not this element's
145
+ * while the menu is closed, so a dialog around the menu still closes on it.
146
+ */
147
+ private handleClosedKeydown;
127
148
  private readonly handleTriggerClick;
128
149
  connectedCallback(): void;
129
150
  disconnectedCallback(): void;
130
151
  protected updated(changed: PropertyValues): void;
152
+ /**
153
+ * Names the surface by its trigger, where nothing else names it.
154
+ *
155
+ * By element reference, which is the one direction that crosses: the surface
156
+ * is in this shadow root and the trigger is in the tree around it. A host
157
+ * `aria-labelledby` is carried down by `aria`, and a host `aria-label` or
158
+ * `label` is a name of its own, so each of them clears this reference.
159
+ */
160
+ private nameSurface;
131
161
  /**
132
162
  * Says whether the page showing reserves a column either side of its labels.
133
163
  *
@@ -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 { anchorElement, customElement, DrilldownController, HelixElement, inlineStep, KeyboardNavigationController, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, tellDisclosure, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, HelixElement, inlineStep, KeyboardNavigationController, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, slottedWords, tellDisclosure, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -13,7 +13,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.backLabel = 'Back';
16
- this.label = 'Menu';
16
+ this.label = '';
17
17
  this.offset = 4;
18
18
  this.placement = 'bottom-start';
19
19
  this.drilldown = new DrilldownController(this, {
@@ -22,9 +22,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
22
22
  slot: 'submenu',
23
23
  });
24
24
  this.keys = new KeyboardNavigationController(this, {
25
- items: () => this.items,
25
+ items: () => (this.open ? this.items : []),
26
26
  reserved: () => ['ArrowLeft', 'ArrowRight'],
27
27
  selects: () => false,
28
+ typeahead: (item) => slottedWords(item, ['']),
29
+ });
30
+ this.aria = new AriaForwardController(this, {
31
+ label: true,
32
+ target: () => this.partNamed('surface'),
28
33
  });
29
34
  this.panel = new PopoverController(this, {
30
35
  anchor: () => this.trigger,
@@ -61,8 +66,12 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
61
66
  scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
62
67
  };
63
68
  this.handleKeydown = (event) => {
69
+ if (!this.open) {
70
+ this.handleClosedKeydown(event);
71
+ return;
72
+ }
64
73
  const items = this.items;
65
- const at = items.indexOf(document.activeElement);
74
+ const at = items.findIndex((item) => item.active);
66
75
  const along = inlineStep(this, event.key);
67
76
  if (event.key === 'Escape') {
68
77
  event.stopPropagation();
@@ -94,7 +103,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
94
103
  }
95
104
  tellTrigger() {
96
105
  const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
97
- tellDisclosure(slot?.assignedElements({ flatten: false })[0], this.open, 'menu');
106
+ tellDisclosure(slot?.assignedElements({ flatten: true })[0], this.open, 'menu');
98
107
  }
99
108
  get trigger() {
100
109
  return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this);
@@ -103,11 +112,15 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
103
112
  return this.drilldown.rows.filter((row) => !row.disabled);
104
113
  }
105
114
  show() {
115
+ this.openAt('first');
116
+ }
117
+ openAt(end) {
106
118
  if (this.open || !this.requestOpen(true)) {
107
119
  return;
108
120
  }
109
121
  this.updateComplete.then(() => {
110
- this.items[0]?.focus();
122
+ const items = this.items;
123
+ (end === 'first' ? items[0] : items[items.length - 1])?.focus();
111
124
  });
112
125
  }
113
126
  hide() {
@@ -116,7 +129,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
116
129
  }
117
130
  this.panel.hide();
118
131
  this.drilldown.reset();
119
- if (this.contains(document.activeElement)) {
132
+ if (deepContains(this, deepActiveElement())) {
120
133
  this.trigger.focus();
121
134
  }
122
135
  }
@@ -135,6 +148,18 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
135
148
  this.drilldown.back();
136
149
  this.updateComplete.then(() => leaving?.focus());
137
150
  }
151
+ handleClosedKeydown(event) {
152
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
153
+ return;
154
+ }
155
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
156
+ const triggers = slot?.assignedElements({ flatten: true }) ?? [];
157
+ if (!event.composedPath().some((node) => triggers.includes(node))) {
158
+ return;
159
+ }
160
+ event.preventDefault();
161
+ this.openAt(event.key === 'ArrowDown' ? 'first' : 'last');
162
+ }
138
163
  connectedCallback() {
139
164
  super.connectedCallback();
140
165
  this.addEventListener('helix-select', this.handleSelect, true);
@@ -159,6 +184,21 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
159
184
  }
160
185
  this.toggleState('drilled', !this.drilldown.atRoot);
161
186
  this.markColumns();
187
+ this.nameSurface();
188
+ }
189
+ nameSurface() {
190
+ const surface = this.partNamed('surface');
191
+ if (!surface || !('ariaLabelledByElements' in surface)) {
192
+ return;
193
+ }
194
+ if (this.hasAttribute('aria-labelledby')) {
195
+ return;
196
+ }
197
+ const trigger = this.renderRoot
198
+ ?.querySelector('slot[name="trigger"]')
199
+ ?.assignedElements({ flatten: true })[0];
200
+ const named = this.label !== '' || this.hasAttribute('aria-label');
201
+ surface.ariaLabelledByElements = trigger && !named ? [trigger] : null;
162
202
  }
163
203
  markColumns() {
164
204
  const rows = this.drilldown.rows;
@@ -183,7 +223,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
183
223
  popover="manual"
184
224
  role="menu"
185
225
  aria-busy=${this.loading ? 'true' : nothing}
186
- aria-label=${this.label}
226
+ aria-label=${this.label || nothing}
187
227
  data-drilled=${this.drilldown.atRoot ? nothing : ''}
188
228
  ?hidden=${!this.open}
189
229
  >
@@ -79,6 +79,12 @@ export declare class HelixHeadlessMeter extends HelixElement {
79
79
  * Accessible name for the reading.
80
80
  */
81
81
  label: string;
82
+ /**
83
+ * The locale the percentage is written in, such as `fr` for `72 %`.
84
+ *
85
+ * Absent uses the runtime's own.
86
+ */
87
+ locale: string;
82
88
  /**
83
89
  * Top of the low band.
84
90
  *
@@ -7,10 +7,23 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ const percents = new Map();
11
+ const percent = (share, locale) => {
12
+ let format = percents.get(locale);
13
+ if (!format) {
14
+ format = new Intl.NumberFormat(locale || undefined, {
15
+ maximumFractionDigits: 0,
16
+ style: 'percent',
17
+ });
18
+ percents.set(locale, format);
19
+ }
20
+ return format.format(share);
21
+ };
10
22
  let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
11
23
  constructor() {
12
24
  super(...arguments);
13
25
  this.label = 'Meter';
26
+ this.locale = '';
14
27
  this.max = 100;
15
28
  this.min = 0;
16
29
  this.showValue = false;
@@ -64,7 +77,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
64
77
  return distance === 0 ? 'good' : distance === 1 ? 'fair' : 'poor';
65
78
  }
66
79
  get text() {
67
- return this.valueText || `${Math.round(this.ratio * 100)}%`;
80
+ return this.valueText || percent(this.ratio, this.locale);
68
81
  }
69
82
  willUpdate(changed) {
70
83
  super.willUpdate(changed);
@@ -74,7 +87,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
74
87
  this.internals.ariaValueMax = String(max);
75
88
  this.internals.ariaValueNow = String(this.reading);
76
89
  this.internals.ariaValueText =
77
- this.valueText || (this.beyond ? `${Math.round(this.ratio * 100)}%` : null);
90
+ this.valueText || (this.beyond ? percent(this.ratio, this.locale) : null);
78
91
  }
79
92
  renderRound() {
80
93
  const bounds = this.thresholds;
@@ -134,6 +147,9 @@ __decorate([
134
147
  __decorate([
135
148
  property()
136
149
  ], HelixHeadlessMeter.prototype, "label", void 0);
150
+ __decorate([
151
+ property()
152
+ ], HelixHeadlessMeter.prototype, "locale", void 0);
137
153
  __decorate([
138
154
  property({ type: Number })
139
155
  ], HelixHeadlessMeter.prototype, "low", void 0);
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A set of destinations, with a marker on the current one.
@@ -15,7 +15,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
15
  *
16
16
  * The component moves a marker to the current item. It finds the current item
17
17
  * by reading `aria-current`, so the attribute that screen readers announce also
18
- * places the marker.
18
+ * places the marker. Any value but `false` counts, so a sidebar of sections on
19
+ * one page can mark the section in view with `aria-current="location"`. Where
20
+ * several are marked, `page` wins.
19
21
  *
20
22
  * Set `orientation="vertical"` to stack the destinations in a column, for a
21
23
  * sidebar.
@@ -35,8 +37,14 @@ export declare class HelixHeadlessNavigation extends HelixElement {
35
37
  private readonly indicator;
36
38
  /**
37
39
  * The landmark's name, read by screen readers.
40
+ *
41
+ * An `aria-label` or `aria-labelledby` on the host names the landmark too,
42
+ * and wins over this, so a footer navigation written the usual way is not a
43
+ * second "Main".
38
44
  */
39
45
  label: string;
46
+ /** Carries a host `aria-label` and `aria-labelledby` to the `<nav>`. */
47
+ protected readonly aria: AriaForwardController;
40
48
  /**
41
49
  * Stacks the destinations in a column, for a sidebar, where unset is a row
42
50
  * for a header or a footer. The marker follows the current destination down
@@ -4,19 +4,30 @@ 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, IndicatorController } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixElement, IndicatorController, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.indicator = new IndicatorController(this, {
14
- active: () => this.items.find((item) => item.getAttribute('aria-current') === 'page'),
14
+ active: () => {
15
+ const items = this.items;
16
+ const marked = (item) => {
17
+ const current = item.getAttribute('aria-current');
18
+ return current !== null && current !== 'false';
19
+ };
20
+ return (items.find((item) => item.getAttribute('aria-current') === 'page') ?? items.find(marked));
21
+ },
15
22
  indicator: () => this.partNamed('indicator'),
16
23
  observe: () => this.items,
17
24
  surface: () => this.partNamed('nav'),
18
25
  });
19
26
  this.label = 'Main';
27
+ this.aria = new AriaForwardController(this, {
28
+ label: true,
29
+ target: () => this.partNamed('nav'),
30
+ });
20
31
  }
21
32
  get items() {
22
33
  const slot = this.renderRoot.querySelector('slot');
@@ -33,6 +33,7 @@ declare const HelixHeadlessNotificationGroup_base: (new (...args: any[]) => impo
33
33
  *
34
34
  * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the group collapses. Cancelable.
35
35
  * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the group expands. Cancelable: cancel it to keep the group collapsed.
36
+ * @event {ToggleEvent} toggle - The group expanded or collapsed. The event carries `newState` and `oldState`.
36
37
  *
37
38
  * @csspart actions - The trailing region of the header.
38
39
  * @csspart count - The number of notifications in the group.
@@ -96,6 +96,13 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
96
96
  */
97
97
  get read(): boolean;
98
98
  set read(next: boolean);
99
+ /**
100
+ * What a screen reader hears before an unread notification's text.
101
+ *
102
+ * The unread mark is drawn, so a reader who cannot see it hears this word in
103
+ * its place. Translate it with the rest of your interface.
104
+ */
105
+ unreadLabel: string;
99
106
  /**
100
107
  * @internal Read by the list to identify its children across the light DOM,
101
108
  * which is cheaper than `instanceof` and is why this is public rather than
@@ -192,6 +199,11 @@ export declare class HelixHeadlessNotification extends HelixHeadlessNotification
192
199
  protected render(): TemplateResult;
193
200
  private readonly handleActions;
194
201
  private readonly handleDismiss;
202
+ /**
203
+ * Where focus goes when this row leaves: the next row's first control, the
204
+ * one before, then the first control left in the feed.
205
+ */
206
+ private neighbor;
195
207
  }
196
208
  declare global {
197
209
  interface HTMLElementTagNameMap {
@@ -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, HelixReadEvent, hostAria, MixinDismissible, requestChange, slotFilled, } from '@fusion.dev/helix-core';
7
+ import { customElement, deepActiveElement, deepContains, focusableElements, HelixElement, HelixReadEvent, hostAria, MixinDismissible, requestChange, slotFilled, } from '@fusion.dev/helix-core';
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -17,6 +17,7 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
17
17
  this.at = '';
18
18
  this.defaultRead = false;
19
19
  this.locale = '';
20
+ this.unreadLabel = 'Unread';
20
21
  this.isNotification = true;
21
22
  this.readState = undefined;
22
23
  this.handleActions = () => {
@@ -25,7 +26,9 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
25
26
  this.handleDismiss = (event) => {
26
27
  event.stopPropagation();
27
28
  if (this.dismiss()) {
29
+ const next = deepContains(this, deepActiveElement()) ? this.neighbor() : null;
28
30
  this.remove();
31
+ next?.focus();
29
32
  }
30
33
  };
31
34
  }
@@ -105,7 +108,7 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
105
108
  return html `<div part="item">
106
109
  <span part="media"><slot name="media"></slot></span>
107
110
  <div part="body">
108
- <div part="label"><slot></slot></div>
111
+ <div part="label">${this.read ? nothing : html `<span role="img" aria-label=${this.unreadLabel}></span>`}<slot></slot></div>
109
112
  <slot name="description"></slot>
110
113
  <slot name="detail"></slot>
111
114
  <span part="actions"><slot name="actions" @slotchange=${this.handleActions}></slot></span>
@@ -116,6 +119,29 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
116
119
  </div>
117
120
  </div>`;
118
121
  }
122
+ neighbor() {
123
+ let feed = this.parentElement;
124
+ while (feed && !('markEveryoneRead' in feed)) {
125
+ feed = feed.parentElement;
126
+ }
127
+ const rows = feed
128
+ ? feed.items
129
+ : [...(this.parentElement?.children ?? [])].filter((one) => 'isNotification' in one);
130
+ const at = rows.indexOf(this);
131
+ const order = [...rows.slice(at + 1), ...rows.slice(0, Math.max(at, 0)).reverse()];
132
+ for (const row of order) {
133
+ const target = row.shadowRoot?.querySelector('[part="dismiss"]') ?? focusableElements(row)[0];
134
+ if (target) {
135
+ return target;
136
+ }
137
+ }
138
+ if (!feed) {
139
+ return null;
140
+ }
141
+ return (feed.shadowRoot?.querySelector('[part="mark-all"]') ??
142
+ focusableElements(feed).find((one) => !this.contains(one)) ??
143
+ null);
144
+ }
119
145
  };
120
146
  __decorate([
121
147
  property()
@@ -129,6 +155,9 @@ __decorate([
129
155
  __decorate([
130
156
  property({ type: Boolean })
131
157
  ], HelixHeadlessNotification.prototype, "read", null);
158
+ __decorate([
159
+ property({ attribute: 'unread-label' })
160
+ ], HelixHeadlessNotification.prototype, "unreadLabel", void 0);
132
161
  HelixHeadlessNotification = __decorate([
133
162
  customElement('helix-headless-notification')
134
163
  ], HelixHeadlessNotification);
@@ -202,6 +202,13 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
202
202
  private markEmpty;
203
203
  protected updated(changed: PropertyValues): void;
204
204
  protected render(): TemplateResult;
205
+ /**
206
+ * Marks everything read, and keeps focus in the feed.
207
+ *
208
+ * The button shows only while something is unread, so the press that
209
+ * succeeds removes the button it was made on. Focus moves to the first row's
210
+ * control, or the first control left in the feed.
211
+ */
205
212
  private readonly handleMarkAll;
206
213
  }
207
214
  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, HelixReadEvent, hostAria, MixinCollection, markRegion, } from '@fusion.dev/helix-core';
7
+ import { customElement, focusableElements, HelixElement, HelixReadEvent, hostAria, MixinCollection, markRegion, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
@@ -20,8 +20,23 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
20
20
  this.handleRead = () => {
21
21
  queueMicrotask(() => this.requestUpdate());
22
22
  };
23
- this.handleMarkAll = () => {
23
+ this.handleMarkAll = (event) => {
24
+ const button = event.currentTarget;
25
+ const focused = this.shadowRoot?.activeElement === button;
24
26
  this.markEveryoneRead();
27
+ if (!focused) {
28
+ return;
29
+ }
30
+ void this.updateComplete.then(() => {
31
+ if (button.isConnected) {
32
+ return;
33
+ }
34
+ const target = this.items
35
+ .map((row) => row.shadowRoot?.querySelector('[part="dismiss"]') ??
36
+ focusableElements(row)[0])
37
+ .find((one) => one !== undefined) ?? focusableElements(this)[0];
38
+ target?.focus();
39
+ });
25
40
  };
26
41
  }
27
42
  static { this.shadowRootOptions = {
@@ -18,8 +18,9 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
18
18
  *
19
19
  * `inputmode="decimal"` keeps the numeric keypad on phones.
20
20
  *
21
- * Validity still comes from the platform: `min`, `max`, and `step` are mirrored
22
- * onto the host's own `setValidity`.
21
+ * Validity follows the platform's rules: `min`, `max`, and `step` are checked
22
+ * and reported through the host's own `setValidity`, under the platform's flag
23
+ * names.
23
24
  *
24
25
  * Stepping is separate from typing, so a number is not corrected while you type
25
26
  * it.
@@ -31,6 +32,9 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
31
32
  * @documentation https://fusion.dev/helix/components/number-input
32
33
  *
33
34
  * @slot end - Content after the number, such as a unit.
35
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
36
+ * @slot hint - Help under the control, for content richer than the `hint` text.
37
+ * @slot label - The label, for content richer than the `label` text.
34
38
  * @slot start - Content before the number, such as a unit or an icon.
35
39
  *
36
40
  * @event {Event} change - The user finished changing the value. Relayed from the native field.
@@ -88,6 +92,15 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
88
92
  * what a reader wants from a whole key.
89
93
  */
90
94
  largeStep?: number;
95
+ /**
96
+ * The locale a typed number is read in, such as `de-DE`. Empty reads it in
97
+ * the browser's language.
98
+ *
99
+ * Where the locale writes a decimal comma, `1,5` is read as one and a half,
100
+ * which is what the decimal keypad on a phone in that locale types. A dot is
101
+ * read as a decimal point in every locale.
102
+ */
103
+ locale: string;
91
104
  /**
92
105
  * The largest value allowed.
93
106
  *
@@ -132,14 +145,29 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
132
145
  */
133
146
  scrub: boolean;
134
147
  /**
135
- * How far one step moves.
148
+ * How far one step moves. A value off the step grid, counted from `min`, is
149
+ * invalid, as it is in a native number field.
136
150
  */
137
151
  step: number;
138
- /** Focus is delegated to the field, so the host is never the focus target. */
139
- static shadowRootOptions: ShadowRootInit;
152
+ /**
153
+ * The message the browser shows when the number is off the step grid, with
154
+ * `{below}` and `{above}` filled in with the nearest valid values.
155
+ */
156
+ stepMismatchMessage?: string;
157
+ /**
158
+ * What a screen reader says for the value, with `{value}` filled in, such as
159
+ * `{value} kilograms`.
160
+ *
161
+ * A unit slotted into `end` is drawn beside the number and is not part of
162
+ * what the spinbutton announces, so it goes here too. Empty announces the
163
+ * number alone.
164
+ */
165
+ valueText: string;
140
166
  private get control();
141
167
  /** The value as a number, or `null` where the field holds nothing usable. */
142
168
  private get numeric();
169
+ /** Whether `value` is on the step grid, counted from `min`. */
170
+ private onStep;
143
171
  /**
144
172
  * A value pulled into range and onto the step grid.
145
173
  *
@@ -254,7 +282,19 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
254
282
  * the same as preventing one.
255
283
  */
256
284
  private readonly handleBlur;
257
- /** Relayed, because `change` does not cross a shadow boundary on its own. */
285
+ /**
286
+ * Pulls a typed number into range and onto the step grid, and returns
287
+ * whether that moved the value.
288
+ */
289
+ private settle;
290
+ /**
291
+ * Relayed, because `change` does not cross a shadow boundary on its own.
292
+ *
293
+ * **Settled first.** The field's `change` arrives before its `blur`, so
294
+ * relaying it as it came announced the number as typed, 150 in a field whose
295
+ * `max` is 100, and the clamp that followed sent `input` alone: a consumer
296
+ * saving on `change` kept 150 while the form submitted 100.
297
+ */
258
298
  private readonly handleChange;
259
299
  /**
260
300
  * The four keys a spinner answers, which `Input` does not bind.
@@ -274,7 +314,13 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
274
314
  * message is what a native spinner would have said.
275
315
  */
276
316
  private sync;
277
- /** Puts the cursor in the field. */
317
+ /**
318
+ * Puts the cursor in the field.
319
+ *
320
+ * **No `delegatesFocus`**, because the platform's delegate is the first
321
+ * focusable element, which is the Decrease stepper: a label pressed for the
322
+ * field landed on it, and the arrows then did nothing.
323
+ */
278
324
  focus(options?: FocusOptions): void;
279
325
  /** Selects the number, so typing replaces it. */
280
326
  select(): void;
@@ -287,6 +333,15 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
287
333
  private releaseCaret;
288
334
  protected willUpdate(changed: PropertyValues): void;
289
335
  protected updated(changed: PropertyValues): void;
336
+ /**
337
+ * Enter submits the form, as it does in a native text field.
338
+ *
339
+ * On `keypress`, where the platform runs its own implicit submission, so a
340
+ * consumer who prevents the `keydown` for Enter stops this too.
341
+ */
342
+ private readonly handleKeypress;
343
+ /** @internal A single line of text counts against implicit submission. */
344
+ protected get blocksImplicitSubmission(): boolean;
290
345
  protected render(): TemplateResult;
291
346
  /**
292
347
  * The value as a number, or `NaN` while the control is empty.