@fusion.dev/helix-headless 0.144.0 → 0.145.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 (155) hide show
  1. package/custom-elements.json +8312 -881
  2. package/dist/accordion/item/accordion-item.d.ts +48 -12
  3. package/dist/accordion/item/accordion-item.js +26 -5
  4. package/dist/accordion/root/accordion-root.d.ts +6 -0
  5. package/dist/accordion/root/accordion-root.js +7 -1
  6. package/dist/announcement/root/announcement-root.d.ts +21 -1
  7. package/dist/announcement/root/announcement-root.js +36 -1
  8. package/dist/app-header/root/app-header-root.d.ts +7 -0
  9. package/dist/app-header/root/app-header-root.js +5 -1
  10. package/dist/app-header/sectioning.d.ts +7 -0
  11. package/dist/app-header/sectioning.js +26 -0
  12. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +32 -1
  13. package/dist/breadcrumbs/root/breadcrumbs-root.js +25 -2
  14. package/dist/canvas/root/canvas-root.d.ts +38 -3
  15. package/dist/canvas/root/canvas-root.js +121 -40
  16. package/dist/carousel/root/carousel-root.d.ts +90 -0
  17. package/dist/carousel/root/carousel-root.js +134 -12
  18. package/dist/chat/composer/chat-composer.d.ts +38 -6
  19. package/dist/chat/composer/chat-composer.js +42 -9
  20. package/dist/chat/message/chat-message.d.ts +32 -1
  21. package/dist/chat/message/chat-message.js +25 -9
  22. package/dist/chat/naming.d.ts +22 -0
  23. package/dist/chat/naming.js +12 -0
  24. package/dist/chat/relative-clock.d.ts +39 -0
  25. package/dist/chat/relative-clock.js +31 -0
  26. package/dist/chat/root/chat-root.d.ts +36 -0
  27. package/dist/chat/root/chat-root.js +24 -1
  28. package/dist/code-block/root/code-block-root.d.ts +8 -1
  29. package/dist/code-block/root/code-block-root.js +5 -5
  30. package/dist/copy-button/root/copy-button-root.d.ts +21 -1
  31. package/dist/copy-button/root/copy-button-root.js +2 -2
  32. package/dist/data-table/root/data-table-root.d.ts +150 -2
  33. package/dist/data-table/root/data-table-root.js +303 -29
  34. package/dist/data-table/tree.d.ts +40 -0
  35. package/dist/data-table/tree.js +55 -0
  36. package/dist/description-list/item/description-list-item.d.ts +17 -2
  37. package/dist/description-list/item/description-list-item.js +15 -2
  38. package/dist/description-list/root/description-list-root.d.ts +27 -1
  39. package/dist/description-list/root/description-list-root.js +26 -1
  40. package/dist/drilldown/item/drilldown-item.d.ts +11 -6
  41. package/dist/drilldown/item/drilldown-item.js +8 -5
  42. package/dist/drilldown/root/drilldown-root.d.ts +11 -0
  43. package/dist/drilldown/root/drilldown-root.js +8 -3
  44. package/dist/element-events.generated.js +53 -8
  45. package/dist/filter-input/menu/filter-menu.d.ts +3 -0
  46. package/dist/filter-input/menu/filter-menu.js +8 -4
  47. package/dist/image/root/image-root.js +9 -1
  48. package/dist/input/root/input-root.d.ts +1 -1
  49. package/dist/input/root/input-root.js +2 -2
  50. package/dist/list/item/list-item.d.ts +68 -1
  51. package/dist/list/item/list-item.js +66 -4
  52. package/dist/list/numbering.d.ts +14 -0
  53. package/dist/list/numbering.js +8 -0
  54. package/dist/list/root/list-root.d.ts +106 -9
  55. package/dist/list/root/list-root.js +189 -4
  56. package/dist/list/selection.d.ts +19 -0
  57. package/dist/list/selection.js +21 -0
  58. package/dist/listbox/root/listbox-root.js +1 -2
  59. package/dist/map/root/map-root.d.ts +6 -2
  60. package/dist/map/root/map-root.js +5 -2
  61. package/dist/megamenu/root/megamenu-root.d.ts +21 -0
  62. package/dist/megamenu/root/megamenu-root.js +16 -2
  63. package/dist/menu/index.d.ts +1 -0
  64. package/dist/menu/index.js +1 -0
  65. package/dist/menu/item/menu-item.d.ts +38 -11
  66. package/dist/menu/item/menu-item.js +33 -1
  67. package/dist/menu/root/menu-root.d.ts +25 -1
  68. package/dist/menu/root/menu-root.js +63 -6
  69. package/dist/menu/section/menu-section.d.ts +55 -0
  70. package/dist/menu/section/menu-section.js +60 -0
  71. package/dist/navigation/root/navigation-root.d.ts +16 -1
  72. package/dist/navigation/root/navigation-root.js +27 -1
  73. package/dist/notifications/root/notifications-root.d.ts +18 -0
  74. package/dist/notifications/root/notifications-root.js +17 -1
  75. package/dist/object-inspector/root/object-inspector-root.d.ts +97 -16
  76. package/dist/object-inspector/root/object-inspector-root.js +179 -79
  77. package/dist/object-inspector/rows.d.ts +57 -0
  78. package/dist/object-inspector/rows.js +86 -0
  79. package/dist/outline/levels.d.ts +31 -0
  80. package/dist/outline/levels.js +23 -0
  81. package/dist/outline/root/outline-root.d.ts +68 -3
  82. package/dist/outline/root/outline-root.js +73 -8
  83. package/dist/pagination/size/pagination-size.d.ts +11 -2
  84. package/dist/pagination/size/pagination-size.js +19 -4
  85. package/dist/radio/group/radio-group.d.ts +4 -10
  86. package/dist/radio/group/radio-group.js +5 -10
  87. package/dist/rating/root/rating-root.d.ts +1 -1
  88. package/dist/rating/root/rating-root.js +2 -2
  89. package/dist/scroll-area/root/scroll-area-root.d.ts +113 -0
  90. package/dist/scroll-area/root/scroll-area-root.js +218 -3
  91. package/dist/scroll-area/scrollbar.d.ts +65 -0
  92. package/dist/scroll-area/scrollbar.js +24 -0
  93. package/dist/select/root/select-root.js +1 -1
  94. package/dist/splitter/root/splitter-root.d.ts +13 -0
  95. package/dist/splitter/root/splitter-root.js +20 -2
  96. package/dist/splitter/snap.d.ts +9 -0
  97. package/dist/splitter/snap.js +12 -0
  98. package/dist/steps/reach.d.ts +15 -0
  99. package/dist/steps/reach.js +12 -0
  100. package/dist/steps/root/steps-root.d.ts +30 -0
  101. package/dist/steps/root/steps-root.js +53 -0
  102. package/dist/steps/step/step.d.ts +38 -2
  103. package/dist/steps/step/step.js +28 -6
  104. package/dist/table/column/table-column.d.ts +11 -0
  105. package/dist/table/column/table-column.js +6 -1
  106. package/dist/table/root/table-root.d.ts +101 -0
  107. package/dist/table/root/table-root.js +166 -5
  108. package/dist/table/row/table-row.d.ts +47 -1
  109. package/dist/table/row/table-row.js +39 -2
  110. package/dist/tabs/panel/tab-panel.d.ts +9 -5
  111. package/dist/tabs/panel/tab-panel.js +3 -4
  112. package/dist/tabs/root/tabs-root.d.ts +25 -1
  113. package/dist/tabs/root/tabs-root.js +21 -3
  114. package/dist/tabs/tab/tab.d.ts +36 -4
  115. package/dist/tabs/tab/tab.js +46 -3
  116. package/dist/timeline/group/timeline-group.d.ts +46 -0
  117. package/dist/timeline/group/timeline-group.js +37 -0
  118. package/dist/timeline/index.d.ts +1 -0
  119. package/dist/timeline/index.js +1 -0
  120. package/dist/timeline/root/timeline-root.d.ts +10 -3
  121. package/dist/toast/item/toast-item.d.ts +10 -17
  122. package/dist/toast/item/toast-item.js +15 -36
  123. package/dist/toolbar/root/toolbar-root.d.ts +13 -0
  124. package/dist/toolbar/root/toolbar-root.js +15 -5
  125. package/dist/tree/item/tree-item.d.ts +54 -1
  126. package/dist/tree/item/tree-item.js +89 -20
  127. package/dist/tree/root/tree-root.d.ts +37 -0
  128. package/dist/tree/root/tree-root.js +51 -6
  129. package/dist/tree/selection.d.ts +24 -0
  130. package/dist/tree/selection.js +26 -0
  131. package/dist/video/media-events.d.ts +49 -0
  132. package/dist/video/media-events.js +39 -0
  133. package/dist/video/root/video-root.d.ts +99 -13
  134. package/dist/video/root/video-root.js +84 -47
  135. package/dist/video-playlist/menu/video-playlist-menu.d.ts +3 -0
  136. package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
  137. package/dist/video-playlist/order.d.ts +41 -0
  138. package/dist/video-playlist/order.js +34 -0
  139. package/dist/video-playlist/root/video-playlist-root.d.ts +70 -2
  140. package/dist/video-playlist/root/video-playlist-root.js +60 -16
  141. package/dist/virtual-list/controls.d.ts +4 -0
  142. package/dist/virtual-list/controls.js +30 -0
  143. package/dist/virtual-list/fill.d.ts +30 -0
  144. package/dist/virtual-list/fill.js +39 -0
  145. package/dist/virtual-list/keys.d.ts +15 -0
  146. package/dist/virtual-list/keys.js +24 -0
  147. package/dist/virtual-list/root/virtual-list-root.d.ts +155 -23
  148. package/dist/virtual-list/root/virtual-list-root.js +175 -34
  149. package/lint.json +1 -1
  150. package/package.json +5 -5
  151. package/types/jsx.d.ts +3778 -801
  152. package/types/svelte.d.ts +2 -0
  153. package/types/vue.d.ts +782 -215
  154. package/dist/listbox/range.d.ts +0 -17
  155. package/dist/listbox/range.js +0 -5
@@ -45,7 +45,8 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
45
45
  * for each layer, which sets the layer's `hidden` attribute. Layers with the
46
46
  * same `group`, such as a route and its dotted legs, share one tile. A tile
47
47
  * sends `helix-before-toggle` from the layer first, so you can store the
48
- * choice or refuse it.
48
+ * choice or refuse it, and `helix-toggle` from it once `hidden` holds the
49
+ * choice.
49
50
  *
50
51
  * Imagery, a `helix-map-raster` with `ground`, is a map type and not a tile.
51
52
  * The menu opens with a card for each type, Standard, Hybrid, and Satellite,
@@ -80,6 +81,7 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
80
81
  * @event {HelixMapRotateEvent} helix-map-rotate - A rotate or Face north button, or Shift with Left or Right, was pressed. Turn your map in response.
81
82
  * @event {HelixMapTiltEvent} helix-map-tilt - The 3D button, or Shift with Up or Down, was pressed. Tilt your map in response.
82
83
  * @event {HelixMapZoomEvent} helix-map-zoom - A zoom button or key was pressed, with a direction. Zoom your map in response.
84
+ * @event {HelixToggleEvent} helix-toggle - A layer's tile in the layers menu showed or hid it. Sent from the layer once its `hidden` attribute holds the choice, with `checked` true when it is shown.
83
85
  *
84
86
  * @csspart attribution - The strip under the map that holds the credit.
85
87
  * @csspart compass - The compass and the pan and rotate buttons around it.
@@ -291,7 +293,9 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
291
293
  * **Asked first, from the layer.** The tile wrote `hidden` onto the
292
294
  * consumer's element and said nothing, so a page could neither keep the
293
295
  * choice nor keep its own state in step. The request comes from the first
294
- * layer of the tile, which is the element the page wrote.
296
+ * layer of the tile, which is the element the page wrote. Once `hidden`
297
+ * holds the answer, the same layer sends `helix-toggle`, the request's verb
298
+ * without `before`, since the platform has no word for a layer shown.
295
299
  */
296
300
  private showLayers;
297
301
  /** Asks for a map type, and sends `change` once it holds. */
@@ -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, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, } from '@fusion.dev/helix-core';
7
+ import { customElement, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixToggleEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, } from '@fusion.dev/helix-core';
8
8
  import { buildings, cloudRain, dotsNine, leaf, mountains, path, polygon, stack, target, } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -210,7 +210,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
210
210
  if (!first) {
211
211
  return;
212
212
  }
213
- requestChange({
213
+ const applied = requestChange({
214
214
  apply: () => {
215
215
  for (const layer of layers) {
216
216
  layer.hidden = !shown;
@@ -221,6 +221,9 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
221
221
  host: first,
222
222
  next: shown,
223
223
  });
224
+ if (applied) {
225
+ first.dispatchEvent(new HelixToggleEvent({ checked: shown }));
226
+ }
224
227
  }
225
228
  chooseMapType(type) {
226
229
  const applied = requestChange({
@@ -20,6 +20,13 @@ declare const HelixHeadlessMegamenu_base: (new (...args: any[]) => import("@fusi
20
20
  * which. Hover is the default because in a navigation bar people expect to move
21
21
  * along the row and see each section.
22
22
  *
23
+ * When one panel opens in place of another, each says which way the reader
24
+ * moved, as Radix's `data-motion` does: the one opening is
25
+ * `:state(from-start)` or `:state(from-end)`, by where the one closing sits in
26
+ * the bar, and the one closing is `:state(to-start)` or `:state(to-end)`. A
27
+ * stylesheet slides them on that. Put megamenus in a `Navigation` with
28
+ * `arrows` and the arrow keys move between their triggers.
29
+ *
23
30
  * @since 0.1.0
24
31
  * @status stable
25
32
  * @category navigation
@@ -38,8 +45,12 @@ declare const HelixHeadlessMegamenu_base: (new (...args: any[]) => import("@fusi
38
45
  *
39
46
  * @cssproperty --megamenu-top - Where the panel starts, measured from the bar above it.
40
47
  *
48
+ * @cssstate from-end - The panel opened in place of one later in the bar, so it can enter from that side.
49
+ * @cssstate from-start - The panel opened in place of one earlier in the bar.
41
50
  * @cssstate narrow - The page is narrower than `band-below`, so the panel is shown as a band whatever its variant.
42
51
  * @cssstate open - The panel is open, and the trigger is shown as current.
52
+ * @cssstate to-end - The panel closed because one later in the bar opened, so it can leave toward it.
53
+ * @cssstate to-start - The panel closed because one earlier in the bar opened.
43
54
  */
44
55
  export declare class HelixHeadlessMegamenu extends HelixHeadlessMegamenu_base {
45
56
  /**
@@ -119,6 +130,11 @@ export declare class HelixHeadlessMegamenu extends HelixHeadlessMegamenu_base {
119
130
  private get slotted();
120
131
  /** Whatever the consumer slotted, or the button rendered in its place. */
121
132
  private get trigger();
133
+ /**
134
+ * @internal The control a `Navigation`'s arrow keys move to: the trigger
135
+ * the consumer slotted, or the button drawn in its place.
136
+ */
137
+ get navigationControl(): HTMLElement;
122
138
  /** Shuts it and puts the reader back where they were. */
123
139
  private close;
124
140
  /**
@@ -241,6 +257,11 @@ export declare class HelixHeadlessMegamenu extends HelixHeadlessMegamenu_base {
241
257
  * overruled.
242
258
  */
243
259
  protected willUpdate(changed: PropertyValues): void;
260
+ /**
261
+ * Says which way the reader moved when one panel took over from another.
262
+ * Nothing clears all four, for a panel opened on its own.
263
+ */
264
+ private motion;
244
265
  protected updated(changed: PropertyValues): void;
245
266
  /**
246
267
  * The control that opens it, behind a seam the styled tier can move.
@@ -117,6 +117,9 @@ let HelixHeadlessMegamenu = class HelixHeadlessMegamenu extends MixinControllerO
117
117
  get trigger() {
118
118
  return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this.partNamed('trigger') ?? this);
119
119
  }
120
+ get navigationControl() {
121
+ return this.trigger;
122
+ }
120
123
  close() {
121
124
  if (!this.requestOpen(false)) {
122
125
  return;
@@ -184,8 +187,13 @@ let HelixHeadlessMegamenu = class HelixHeadlessMegamenu extends MixinControllerO
184
187
  return;
185
188
  }
186
189
  if (this.open) {
187
- if (showing && showing !== this) {
188
- showing.requestOpen(false);
190
+ const previous = showing && showing !== this ? showing : undefined;
191
+ const forward = previous !== undefined &&
192
+ (previous.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
193
+ this.motion(previous ? (forward ? 'from-end' : 'from-start') : undefined);
194
+ if (previous) {
195
+ previous.motion(forward ? 'to-end' : 'to-start');
196
+ previous.requestOpen(false);
189
197
  }
190
198
  showing = this;
191
199
  }
@@ -193,6 +201,12 @@ let HelixHeadlessMegamenu = class HelixHeadlessMegamenu extends MixinControllerO
193
201
  showing = undefined;
194
202
  }
195
203
  }
204
+ motion(to) {
205
+ this.toggleState('from-end', to === 'from-end');
206
+ this.toggleState('from-start', to === 'from-start');
207
+ this.toggleState('to-end', to === 'to-end');
208
+ this.toggleState('to-start', to === 'to-start');
209
+ }
196
210
  updated(changed) {
197
211
  super.updated?.(changed);
198
212
  if (changed.has('bandBelow')) {
@@ -1,2 +1,3 @@
1
1
  export * from './item/menu-item.js';
2
2
  export * from './root/menu-root.js';
3
+ export * from './section/menu-section.js';
@@ -1,2 +1,3 @@
1
1
  export * from './item/menu-item.js';
2
2
  export * from './root/menu-root.js';
3
+ export * from './section/menu-section.js';
@@ -1,5 +1,6 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
+ import '../../kbd/index.js';
3
4
  /**
4
5
  * @summary One row in a menu: a command, a choice, or a way into a submenu.
5
6
  *
@@ -23,21 +24,30 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
23
24
  * attribute. An attribute written onto your element after it upgrades causes a
24
25
  * hydration mismatch.
25
26
  *
27
+ * `shortcut` shows a row's keyboard shortcut after its label, drawn by `Kbd`
28
+ * in the platform's glyphs, and gives the row `aria-keyshortcuts`. It binds
29
+ * nothing: the shortcut is the page's to bind. Other trailing words, such as a
30
+ * count, go in the `end` slot.
31
+ *
26
32
  * @since 0.1.0
27
33
  * @status stable
28
34
  * @aliases Menu option, Menu row
29
35
  * @documentation https://fusion.dev/helix/components/menu
30
36
  *
31
37
  * @slot - The label.
38
+ * @slot end - Words after the label, such as a count or a detail, beside any `shortcut`.
32
39
  * @slot start - Content before the label, such as an icon.
33
40
  * @slot submenu - The rows of a submenu that this row opens.
34
41
  *
35
42
  * @event {HelixBeforeToggleEvent} helix-before-toggle - A choice row is about to be checked or unchecked, carrying the state asked for. Cancelable: cancel it to keep the row as it is.
36
43
  * @event {HelixSelectEvent} helix-select - The row was chosen. Cancelable: cancel it to keep the menu open.
37
44
  *
45
+ * @csspart end - The trailing column that holds the shortcut and the `end` slot. It is always rendered.
38
46
  * @csspart item - The row, which is an `<a>` where `href` is set.
39
47
  * @csspart mark - The trailing mark: a check mark when the row is checked, or a caret
40
48
  * when it opens a submenu. A row never shows both, so they share one column.
49
+ * @csspart shortcut - The key drawn for `shortcut`.
50
+ * @csspart shortcut-kbd - The key inside it.
41
51
  * @csspart start - The leading column that holds the `start` slot. It is always rendered.
42
52
  *
43
53
  * @cssstate checked - The row is checked.
@@ -100,19 +110,16 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
100
110
  * roves, which is the same maneuver `Tree.Item` makes and the reason `href`
101
111
  * changes the markup rather than only adding an attribute.
102
112
  *
103
- * **`Option` cannot have this and the reason is worth knowing**, since a
104
- * command palette is the obvious place to want it. A listbox has to keep
105
- * focus somewhere else, in the text field for a combobox, so it says which
106
- * option is current with `aria-activedescendant`. That reference can only
107
- * point at the option's host, measured: assigning
108
- * `ariaActiveDescendantElement` an element inside a sibling shadow root reads
109
- * back `null`. So `role="option"` is pinned to the host, and an anchor under
110
- * it hits the naming failure above. A menu has no such constraint because
111
- * focus really moves to the row.
113
+ * **`Option` takes an `href` too, and keeps its role a different way.** A
114
+ * listbox keeps focus somewhere else, in the text field for a combobox, and
115
+ * names the current option with `aria-activedescendant`, which can only point
116
+ * at the option's host. So an option stays `role="option"` on its host, draws
117
+ * no anchor, and follows its link through an anchor made for the moment. A
118
+ * menu has no such constraint, because focus really moves to the row.
112
119
  */
113
120
  href: string;
114
121
  /**
115
- * Whether the row is on the menu page that is showing.
122
+ * @internal Whether the row is on the menu page that is showing.
116
123
  *
117
124
  * Set by the menu, and a property rather than an attribute for that reason.
118
125
  * Public because the menu has to reach it, and not something a consumer
@@ -124,7 +131,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
124
131
  */
125
132
  rel: string;
126
133
  /**
127
- * Whether the page showing is this row's submenu, or one below it.
134
+ * The row's keyboard shortcut as a chord, such as `mod+s`, shown after the
135
+ * label in the platform's glyphs and given to the row as
136
+ * `aria-keyshortcuts`. `mod` is Command on an Apple keyboard and Control
137
+ * elsewhere. Showing a shortcut does not bind it.
138
+ */
139
+ shortcut: string;
140
+ /**
141
+ * @internal Whether the page showing is this row's submenu, or one below it.
128
142
  *
129
143
  * Separate from `onPage`, and it has to be. A row that opened a page is not
130
144
  * itself on that page, so both are false for it, and rendering nothing on
@@ -185,6 +199,8 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
185
199
  * exactly the thing that is.
186
200
  */
187
201
  private start;
202
+ /** Whether anything is slotted after the label, read the way `start` is. */
203
+ private end;
188
204
  /**
189
205
  * Both answers, from the children, before each render.
190
206
  *
@@ -205,6 +221,17 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
205
221
  * is never rebuilt and never fires on its way out.
206
222
  */
207
223
  private readonly readStart;
224
+ /** Re-reads whether anything is after the label, as `readStart` does before it. */
225
+ private readonly readEnd;
226
+ /**
227
+ * The trailing column: the shortcut's key, then whatever is slotted in `end`.
228
+ * Drawn by `Kbd`, interpolated so each tier draws its own.
229
+ *
230
+ * The key is hidden from the accessibility tree, since the row's
231
+ * `aria-keyshortcuts` says it in words a screen reader reads as keys: inside
232
+ * the row its glyphs became part of the row's name.
233
+ */
234
+ protected renderEnd(): TemplateResult;
208
235
  /**
209
236
  * Re-reads whether anything is in the submenu, from whichever slot is current.
210
237
  *
@@ -4,11 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, deepActiveElement, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, requestChange, } from '@fusion.dev/helix-core';
7
+ import { ariaChord, customElement, deepActiveElement, HelixBeforeToggleEvent, HelixElement, HelixSelectEvent, onApple, requestChange, } from '@fusion.dev/helix-core';
8
8
  import { caretRight, check } 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
+ import { html as staticHtml } from 'lit/static-html.js';
13
+ import '../../kbd/index.js';
12
14
  let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
13
15
  constructor() {
14
16
  super(...arguments);
@@ -20,16 +22,22 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
20
22
  this.href = '';
21
23
  this.onPage = true;
22
24
  this.rel = '';
25
+ this.shortcut = '';
23
26
  this.showingSubmenu = false;
24
27
  this.target = '';
25
28
  this.value = '';
26
29
  this.isMenuItem = true;
27
30
  this.submenu = false;
28
31
  this.start = false;
32
+ this.end = false;
29
33
  this.readStart = () => {
30
34
  const slot = this.renderRoot.querySelector('slot[name="start"]');
31
35
  this.start = (slot?.assignedNodes().length ?? 0) > 0;
32
36
  };
37
+ this.readEnd = () => {
38
+ const slot = this.renderRoot.querySelector('slot[name="end"]');
39
+ this.end = (slot?.assignedNodes().length ?? 0) > 0;
40
+ };
33
41
  this.readSubmenu = () => {
34
42
  const slot = this.renderRoot.querySelector('slot[name="submenu"]');
35
43
  this.submenu = (slot?.assignedElements().length ?? 0) > 0;
@@ -65,11 +73,25 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
65
73
  const named = (name) => this.knownChildren.some((child) => child.slot === name);
66
74
  this.submenu = named('submenu');
67
75
  this.start = named('start');
76
+ this.end = named('end');
68
77
  }
69
78
  firstUpdated(changed) {
70
79
  super.firstUpdated(changed);
71
80
  this.finishHydration();
72
81
  }
82
+ renderEnd() {
83
+ const kbd = this.tag('kbd');
84
+ return html `<span part="end" ?data-empty=${!this.end && this.shortcut === ''}
85
+ >${this.shortcut === ''
86
+ ? nothing
87
+ : staticHtml `<${kbd}
88
+ part="shortcut"
89
+ exportparts="kbd: shortcut-kbd"
90
+ aria-hidden="true"
91
+ keys=${this.shortcut}
92
+ ></${kbd}>`}<slot name="end" @slotchange=${this.readEnd}></slot
93
+ ></span>`;
94
+ }
73
95
  focus() {
74
96
  this.partNamed('item')?.focus();
75
97
  }
@@ -142,10 +164,12 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
142
164
  const ticks = this.checkable || this.checked;
143
165
  const role = ticks ? (this.exclusive ? 'menuitemradio' : 'menuitemcheckbox') : 'menuitem';
144
166
  const checkedState = ticks ? (this.checked ? 'true' : 'false') : nothing;
167
+ const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
145
168
  const inside = html `<span part="start" ?data-empty=${!this.start}
146
169
  ><slot name="start" @slotchange=${this.readStart}></slot
147
170
  ></span>
148
171
  <slot></slot>
172
+ ${this.renderEnd()}
149
173
  ${submenu
150
174
  ? html `<svg
151
175
  part="mark"
@@ -177,6 +201,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
177
201
  role=${role}
178
202
  aria-checked=${checkedState}
179
203
  aria-haspopup=${submenu ? 'menu' : nothing}
204
+ aria-keyshortcuts=${keys}
180
205
  tabindex="-1"
181
206
  @click=${this.handleClick}
182
207
  @keydown=${this.handleKeydown}
@@ -187,6 +212,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
187
212
  aria-checked=${checkedState}
188
213
  aria-disabled=${this.disabled ? 'true' : nothing}
189
214
  aria-haspopup=${submenu ? 'menu' : nothing}
215
+ aria-keyshortcuts=${keys}
190
216
  tabindex=${this.disabled ? nothing : '-1'}
191
217
  @click=${this.handleClick}
192
218
  @keydown=${this.handleKeydown}
@@ -218,6 +244,9 @@ __decorate([
218
244
  __decorate([
219
245
  property()
220
246
  ], HelixHeadlessMenuItem.prototype, "rel", void 0);
247
+ __decorate([
248
+ property()
249
+ ], HelixHeadlessMenuItem.prototype, "shortcut", void 0);
221
250
  __decorate([
222
251
  property({ attribute: false })
223
252
  ], HelixHeadlessMenuItem.prototype, "showingSubmenu", void 0);
@@ -233,6 +262,9 @@ __decorate([
233
262
  __decorate([
234
263
  state()
235
264
  ], HelixHeadlessMenuItem.prototype, "start", void 0);
265
+ __decorate([
266
+ state()
267
+ ], HelixHeadlessMenuItem.prototype, "end", void 0);
236
268
  HelixHeadlessMenuItem = __decorate([
237
269
  customElement('helix-headless-menu-item')
238
270
  ], HelixHeadlessMenuItem);
@@ -24,6 +24,16 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
24
24
  * sets this on items as a property, never as an attribute. An attribute written
25
25
  * onto your element after it upgrades causes a hydration mismatch.
26
26
  *
27
+ * Group rows under a heading with `Menu.Section`, which is a `group` named by
28
+ * its heading. Between runs of rows, a `<helix-divider announced>` is a
29
+ * separator a screen reader announces.
30
+ *
31
+ * With `context`, the trigger is an area and the menu is its context menu: a
32
+ * right-click, a long press, Shift+F10 or the menu key over the area opens it
33
+ * at the pointer, or at the area's corner from the keyboard, as Radix's context
34
+ * menu does. A press on the area does not open it, and closing it returns focus
35
+ * to where it was.
36
+ *
27
37
  * @since 0.1.0
28
38
  * @status stable
29
39
  * @category overlays
@@ -31,7 +41,7 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
31
41
  * @documentation https://fusion.dev/helix/components/menu
32
42
  *
33
43
  * @slot - The items.
34
- * @slot trigger - The control that opens the menu.
44
+ * @slot trigger - The control that opens the menu, or with `context` the area a context menu opens over.
35
45
  *
36
46
  * @event {HelixBeforeCloseEvent} helix-before-close - The menu is about to close. Cancelable: cancel it to keep it open.
37
47
  * @event {HelixBeforeOpenEvent} helix-before-open - The menu is about to open. Cancelable: cancel it to keep it closed.
@@ -57,6 +67,11 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
57
67
  * The text of the row that returns from a submenu.
58
68
  */
59
69
  backLabel: string;
70
+ /**
71
+ * Whether the menu is the trigger's context menu: it opens where the reader
72
+ * asks for one, with a right-click, a long press, Shift+F10 or the menu key.
73
+ */
74
+ context: boolean;
60
75
  /**
61
76
  * Accessible name for the surface.
62
77
  *
@@ -104,6 +119,10 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
104
119
  protected readonly keys: KeyboardNavigationController;
105
120
  /** Carries a host `aria-label` and `aria-labelledby` to the surface. */
106
121
  protected readonly aria: AriaForwardController;
122
+ /** Where a context menu was asked for, in the viewport, while it is open. */
123
+ private point;
124
+ /** Where focus was when a context menu opened, so closing it returns there. */
125
+ private returnTo;
107
126
  private readonly panel;
108
127
  /**
109
128
  * Tells a slotted trigger the disclosure state, by property.
@@ -124,6 +143,11 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
124
143
  private openAt;
125
144
  /** Closes the menu and returns it to its first page. */
126
145
  hide(): void;
146
+ /**
147
+ * Opens a context menu where the reader asked for it: at the pointer, or at
148
+ * the area's corner where the keyboard asked and the event has no position.
149
+ */
150
+ private readonly handleContextMenu;
127
151
  /** Shows an item's submenu in place of the current page. */
128
152
  private enter;
129
153
  /** Returns to the page above, or closes when there is none. */
@@ -9,16 +9,20 @@ import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ const isSection = (element) => 'isMenuSection' in element && element.isMenuSection === true;
12
13
  let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinControllerOpen(HelixElement)) {
13
14
  constructor() {
14
15
  super(...arguments);
15
16
  this.backLabel = 'Back';
17
+ this.context = false;
16
18
  this.label = '';
17
19
  this.offset = 4;
18
20
  this.placement = 'bottom-start';
19
21
  this.drilldown = new DrilldownController(this, {
20
22
  isRow: (element) => 'isMenuItem' in element && element.isMenuItem === true,
21
- root: () => [...this.children].filter((child) => child.slot !== 'trigger'),
23
+ root: () => [...this.children]
24
+ .filter((child) => child.slot !== 'trigger')
25
+ .flatMap((child) => (isSection(child) ? [...child.children] : [child])),
22
26
  slot: 'submenu',
23
27
  });
24
28
  this.keys = new KeyboardNavigationController(this, {
@@ -32,7 +36,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
32
36
  target: () => this.partNamed('surface'),
33
37
  });
34
38
  this.panel = new PopoverController(this, {
35
- anchor: () => this.trigger,
39
+ anchor: () => this.context && this.point
40
+ ? this.renderRoot?.querySelector('#point')
41
+ : this.trigger,
36
42
  boundary: () => this,
37
43
  offset: () => this.offset,
38
44
  onDismiss: () => this.hide(),
@@ -40,6 +46,28 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
40
46
  placement: () => this.placement,
41
47
  surface: () => this.partNamed('surface'),
42
48
  });
49
+ this.handleContextMenu = (event) => {
50
+ if (!this.context) {
51
+ return;
52
+ }
53
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
54
+ const areas = slot?.assignedElements({ flatten: true }) ?? [];
55
+ if (!event.composedPath().some((node) => areas.includes(node))) {
56
+ return;
57
+ }
58
+ event.preventDefault();
59
+ const box = this.trigger.getBoundingClientRect();
60
+ const keyed = event.clientX === 0 && event.clientY === 0;
61
+ this.point = keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY };
62
+ if (this.open) {
63
+ this.requestUpdate();
64
+ this.updateComplete.then(() => this.panel.reposition());
65
+ return;
66
+ }
67
+ const active = deepActiveElement();
68
+ this.returnTo = active instanceof HTMLElement ? active : undefined;
69
+ this.show();
70
+ };
43
71
  this.handleSelect = (event) => {
44
72
  const item = event.target;
45
73
  if (!this.items.includes(item)) {
@@ -93,6 +121,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
93
121
  event.preventDefault();
94
122
  };
95
123
  this.handleTriggerClick = () => {
124
+ if (this.context) {
125
+ return;
126
+ }
96
127
  if (this.open) {
97
128
  this.hide();
98
129
  }
@@ -130,8 +161,11 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
130
161
  this.panel.hide();
131
162
  this.drilldown.reset();
132
163
  if (deepContains(this, deepActiveElement())) {
133
- this.trigger.focus();
164
+ ;
165
+ (this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
134
166
  }
167
+ this.point = undefined;
168
+ this.returnTo = undefined;
135
169
  }
136
170
  enter(item) {
137
171
  if (!this.drilldown.enter(item)) {
@@ -149,7 +183,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
149
183
  this.updateComplete.then(() => leaving?.focus());
150
184
  }
151
185
  handleClosedKeydown(event) {
152
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
186
+ if (this.context || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) {
153
187
  return;
154
188
  }
155
189
  const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
@@ -165,17 +199,30 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
165
199
  this.addEventListener('helix-select', this.handleSelect, true);
166
200
  this.addEventListener('keydown', this.handleKeydown);
167
201
  this.addEventListener('focusin', this.handleFocusIn);
202
+ this.addEventListener('contextmenu', this.handleContextMenu);
168
203
  }
169
204
  disconnectedCallback() {
170
205
  super.disconnectedCallback();
171
206
  this.removeEventListener('helix-select', this.handleSelect, true);
172
207
  this.removeEventListener('keydown', this.handleKeydown);
173
208
  this.removeEventListener('focusin', this.handleFocusIn);
209
+ this.removeEventListener('contextmenu', this.handleContextMenu);
174
210
  }
175
211
  updated(changed) {
176
212
  super.updated(changed);
177
- this.tellTrigger();
213
+ if (!this.context) {
214
+ this.tellTrigger();
215
+ }
178
216
  for (const child of this.children) {
217
+ if (isSection(child)) {
218
+ child.onPage = this.drilldown.atRoot;
219
+ for (const row of child.children) {
220
+ if (row.isMenuItem === true) {
221
+ this.mark(row, this.drilldown.atRoot);
222
+ }
223
+ }
224
+ continue;
225
+ }
179
226
  const item = child;
180
227
  if (item.isMenuItem !== true) {
181
228
  continue;
@@ -217,7 +264,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
217
264
  }
218
265
  }
219
266
  render() {
220
- return html `<slot name="trigger" @click=${this.handleTriggerClick}></slot>
267
+ const point = this.context && this.point;
268
+ return html `<slot name="trigger" @click=${this.handleTriggerClick}></slot>${point
269
+ ? html `<span
270
+ id="point"
271
+ aria-hidden="true"
272
+ style="position: fixed; left: 0; top: 0; translate: ${point.x}px ${point.y}px"
273
+ ></span>`
274
+ : nothing}
221
275
  <div
222
276
  part="surface"
223
277
  popover="manual"
@@ -250,6 +304,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
250
304
  __decorate([
251
305
  property({ attribute: 'back-label' })
252
306
  ], HelixHeadlessMenu.prototype, "backLabel", void 0);
307
+ __decorate([
308
+ property({ reflect: true, type: Boolean })
309
+ ], HelixHeadlessMenu.prototype, "context", void 0);
253
310
  __decorate([
254
311
  property()
255
312
  ], HelixHeadlessMenu.prototype, "label", void 0);
@@ -0,0 +1,55 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
+ /**
4
+ * @summary A group of rows in a menu, under a heading.
5
+ *
6
+ * @description
7
+ * The rows inside it are the menu's rows: the arrow keys and typed letters move
8
+ * through them with the rest, and a set of `exclusive` rows is still the run
9
+ * of them next to each other. The section is a `group` named by its heading,
10
+ * as React Aria's menu section is, so a screen reader says which group a row
11
+ * is in as it arrives.
12
+ *
13
+ * A section goes on the menu's first page. A submenu's page takes rows and
14
+ * dividers.
15
+ *
16
+ * @since 0.1.0
17
+ * @status stable
18
+ * @aliases Menu group, Menu heading
19
+ * @documentation https://fusion.dev/helix/components/menu
20
+ *
21
+ * @slot - The rows.
22
+ * @slot heading - The heading, for content richer than `label`.
23
+ *
24
+ * @csspart heading - The words that name the section.
25
+ * @csspart section - The group that holds the heading and the rows.
26
+ */
27
+ export declare class HelixHeadlessMenuSection extends HelixElement {
28
+ /**
29
+ * The heading's words, when they are plain text. Content in the `heading`
30
+ * slot replaces it.
31
+ */
32
+ label: string;
33
+ /**
34
+ * @internal Set by the menu: whether the section is on the page showing. A
35
+ * property, because the menu writes it after the section upgrades.
36
+ */
37
+ onPage: boolean;
38
+ /** @internal What a menu reads to tell a section from a row. */
39
+ readonly isMenuSection = true;
40
+ /**
41
+ * Whether anything is slotted as the heading, read from the children before
42
+ * each render and from the slot after. A section with neither this nor a
43
+ * `label` draws no heading.
44
+ */
45
+ private heading;
46
+ protected willUpdate(changed: PropertyValues): void;
47
+ protected firstUpdated(changed: PropertyValues): void;
48
+ private readonly readHeading;
49
+ protected render(): TemplateResult | typeof nothing;
50
+ }
51
+ declare global {
52
+ interface HTMLElementTagNameMap {
53
+ 'helix-headless-menu-section': HelixHeadlessMenuSection;
54
+ }
55
+ }