@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
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
@@ -26,6 +26,8 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
26
26
  * @cssstate selected - The item is chosen.
27
27
  */
28
28
  export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
29
+ /** A click on the element itself, `click()` included, chooses it, as on `Radio`. */
30
+ protected readonly clicks: HostClickController;
29
31
  /**
30
32
  * Whether the markup declares the item chosen. A form reset returns to it.
31
33
  *
@@ -56,23 +58,25 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
56
58
  get defaultSelected(): boolean;
57
59
  set defaultSelected(next: boolean);
58
60
  /**
59
- * Whether the item holds the group's tab stop while nothing is chosen.
61
+ * Whether the item holds the group's tab stop. Unset, the chosen item holds
62
+ * it.
60
63
  *
61
- * The stop normally rides on the selection, and an unanswered group has no
62
- * selection, so without this the keyboard cannot reach it. Written by the
63
- * group, with no attribute: it is coordination between elements, and
64
- * `tabindex` on a host the consumer rendered is the change a React consumer's
65
- * hydration reports and does not patch up.
64
+ * Written by the group on every item: `true` for the chosen item, or for the
65
+ * first item that can be chosen while there is no selection to carry the
66
+ * stop, and `false` for every item, the chosen one too, while a toolbar the
67
+ * group sits in holds the stop. No attribute: it is coordination between
68
+ * elements, and `tabindex` on a host the consumer rendered is the change a
69
+ * React consumer's hydration reports and does not patch up.
66
70
  */
67
- focusable: boolean;
71
+ focusable?: boolean;
68
72
  /**
69
73
  * Asks for the render that follows a hydration, so the stop stops being every
70
74
  * choice's.
71
75
  *
72
76
  * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
73
77
  * false, which is what keeps the hydrating render the server's. Nothing else
74
- * would ask: the group writes an already-false `focusable` onto every choice
75
- * but one, which changes nothing and so re-renders nothing.
78
+ * is sure to ask: a write from the group that leaves `focusable` as it was
79
+ * re-renders nothing.
76
80
  */
77
81
  protected firstUpdated(changed: PropertyValues): void;
78
82
  protected render(): TemplateResult;
@@ -4,13 +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, HelixElement, MixinSelectableItem } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HostClickController, MixinSelectableItem, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(HelixElement) {
11
11
  constructor() {
12
12
  super(...arguments);
13
- this.focusable = false;
13
+ this.clicks = new HostClickController(this, () => this.partNamed('item'));
14
14
  }
15
15
  get defaultSelected() {
16
16
  return this.hasAttribute('selected');
@@ -29,7 +29,7 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
29
29
  role="radio"
30
30
  aria-checked=${this.selected ? 'true' : 'false'}
31
31
  ?disabled=${this.disabled}
32
- tabindex=${this.selected || this.focusable || !this.childrenKnown ? '0' : '-1'}
32
+ tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
33
33
  @click=${() => this.select()}
34
34
  >
35
35
  <slot></slot>
@@ -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 group of controls that share one tab stop.
@@ -9,7 +9,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
9
9
  *
10
10
  * The toolbar is one tab stop, and the arrow keys move between its controls, so
11
11
  * three toolbars take three presses of Tab to pass. Any element the browser can
12
- * focus counts as a control, including your own elements.
12
+ * focus counts as a control, including your own elements. A child that takes no
13
+ * focus, such as a divider, is skipped. A menu is reached at its trigger.
14
+ *
15
+ * A nested composite, such as a toggle group, is one stop on the toolbar's
16
+ * axis: the toolbar's arrows move past it, and the arrows across the toolbar
17
+ * move inside it. A composite with a `focusable` property, as Toggle Group
18
+ * has, gives up its own tab stop while the toolbar's is elsewhere, so Tab
19
+ * stops once in the toolbar. A composite without one keeps its own tab stop.
13
20
  *
14
21
  * @since 0.1.0
15
22
  * @status stable
@@ -24,27 +31,50 @@ import { type PropertyValues, type TemplateResult } from 'lit';
24
31
  export declare class HelixHeadlessToolbar extends HelixElement {
25
32
  /**
26
33
  * The toolbar's name, read by screen readers.
34
+ *
35
+ * An `aria-label` or `aria-labelledby` on the host names the toolbar too,
36
+ * and wins over this.
27
37
  */
28
38
  label: string;
39
+ /** Carries a host `aria-label` and `aria-labelledby` to the toolbar. */
40
+ protected readonly aria: AriaForwardController;
41
+ /**
42
+ * Whether the stop has been written onto controls this package does not
43
+ * know, which happens on the first focus. From then on, every move of the
44
+ * stop has to write them too, or the one at `0` can be removed or disabled
45
+ * and leave every other one at `-1`.
46
+ */
47
+ private wroteForeign;
29
48
  /**
30
49
  * The direction the arrow keys move focus in.
31
50
  */
32
51
  orientation?: 'horizontal' | 'vertical';
33
52
  private watcher;
34
53
  /**
35
- * Children that can take the tab stop, in document order.
36
- *
37
- * Every child that is not `disabled` and not `aria-hidden`, which is
38
- * deliberately a test on the element rather than a list of tag names: a
39
- * toolbar holds anything, including elements this package has never heard of.
40
- * Client only, there being no tree to walk on the server.
54
+ * The children that take part in the toolbar, in document order, each with
55
+ * the elements in it that take focus.
41
56
  *
42
- * **It does not ask a child whether it is focusable.** Reading a `focusable`
43
- * property is what `setTabStop` does when moving the stop, not what this does
44
- * when choosing the set, so an element that is neither disabled nor hidden and
45
- * still cannot take focus is counted here and skipped there.
57
+ * A test on the element rather than a list of tag names: a toolbar holds
58
+ * anything, including elements this package has never heard of. A child
59
+ * that takes no focus and holds nothing that does is skipped. Client only,
60
+ * there being no tree to walk on the server.
46
61
  */
47
62
  private get controls();
63
+ /** The elements in a child's light DOM that take focus. */
64
+ private within;
65
+ /**
66
+ * The stops of a child that takes focus itself: the child, or the members of
67
+ * a composite that owns its tab stop, such as a toggle group, which the
68
+ * toolbar enters at a member and moves the stop of through `focusable`.
69
+ */
70
+ private inside;
71
+ /**
72
+ * Where focus lands on arriving at a control. A nested composite is entered
73
+ * at the stop it holds, which for a toggle group is the chosen toggle.
74
+ */
75
+ private entry;
76
+ /** The control a press or a focus came from, by its path. */
77
+ private origin;
48
78
  /**
49
79
  * Moves the single tab stop to one control.
50
80
  *
@@ -74,9 +104,10 @@ export declare class HelixHeadlessToolbar extends HelixElement {
74
104
  * already at `-1`, leaving a toolbar the keyboard cannot reach at all. A Save
75
105
  * button disabling itself once the form is clean is enough to do it.
76
106
  *
77
- * `foreign` stays false, the same as the first claim: a control this package
78
- * has never heard of that was never given `tabindex="-1"` is still in the
79
- * document's own tab order, so it is never the one that gets stranded.
107
+ * `foreign` stays false, the same as the first claim, until a focus has
108
+ * written `tabindex="-1"` onto foreign controls. From then on `setTabStop`
109
+ * writes them as well, so removing or disabling the native button that held
110
+ * the stop hands it to the first one left.
80
111
  */
81
112
  private readonly restoreTabStop;
82
113
  /**
@@ -86,6 +117,15 @@ export declare class HelixHeadlessToolbar extends HelixElement {
86
117
  * so a reflected default cannot appear on the host after hydration.
87
118
  */
88
119
  private get axis();
120
+ /**
121
+ * The toolbar's own keys, from inside a nested composite.
122
+ *
123
+ * In capture, ahead of the composite, which answers every arrow itself and
124
+ * would otherwise hold the reader inside it. Only the keys along the
125
+ * toolbar's axis and Home and End are taken, so the arrows across it still
126
+ * move inside the composite.
127
+ */
128
+ private readonly handleCompositeKeydown;
89
129
  private readonly handleKeydown;
90
130
  connectedCallback(): void;
91
131
  disconnectedCallback(): void;
@@ -4,28 +4,63 @@ 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, inlineStep, keyClaimed } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixElement, inlineStep, keyClaimed, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  const KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
11
11
  const ownsTabStop = (node) => 'focusable' in node;
12
+ const NATIVE = 'a[href], button, input, select, textarea, [tabindex], [contenteditable=""], [contenteditable="true"]';
13
+ const TABBABLE = NATIVE.replace('[tabindex]', '[tabindex]:not([tabindex="-1"])');
14
+ const takesFocus = (node) => {
15
+ if (!(node instanceof HTMLElement) ||
16
+ node.hasAttribute('disabled') ||
17
+ node.getAttribute('aria-hidden') === 'true') {
18
+ return false;
19
+ }
20
+ return (node.matches(NATIVE) ||
21
+ ownsTabStop(node) ||
22
+ node.shadowRoot?.delegatesFocus === true ||
23
+ node.shadowRoot?.querySelector(TABBABLE) != null);
24
+ };
12
25
  let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
13
26
  constructor() {
14
27
  super(...arguments);
15
28
  this.label = 'Toolbar';
29
+ this.aria = new AriaForwardController(this, {
30
+ label: true,
31
+ target: () => this.partNamed('toolbar'),
32
+ });
33
+ this.wroteForeign = false;
16
34
  this.handleFocusIn = (event) => {
17
- const control = this.controls.find((entry) => entry.contains(event.target));
35
+ const control = this.origin(event);
18
36
  if (control) {
19
37
  this.setTabStop(control);
20
38
  }
21
39
  };
22
40
  this.restoreTabStop = () => {
23
41
  const controls = this.controls;
24
- const held = controls.some((control) => ownsTabStop(control) ? control.focusable : control.tabIndex === 0);
42
+ const held = controls.some((control) => {
43
+ const composite = control.stops.length !== 1;
44
+ const element = composite ? control.child : control.stops[0];
45
+ if (ownsTabStop(element)) {
46
+ return element.focusable;
47
+ }
48
+ return composite || element.tabIndex === 0;
49
+ });
25
50
  if (controls.length > 0 && !held) {
26
51
  this.setTabStop(controls[0], false);
27
52
  }
28
53
  };
54
+ this.handleCompositeKeydown = (event) => {
55
+ const control = this.origin(event);
56
+ if (!control || control.stops.length === 1) {
57
+ return;
58
+ }
59
+ this.handleKeydown(event);
60
+ if (event.defaultPrevented) {
61
+ event.stopPropagation();
62
+ }
63
+ };
29
64
  this.handleKeydown = (event) => {
30
65
  if (!KEYS.has(event.key) || keyClaimed(event)) {
31
66
  return;
@@ -34,7 +69,8 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
34
69
  const forward = this.axis === 'horizontal' ? along === 1 : event.key === 'ArrowDown';
35
70
  const back = this.axis === 'horizontal' ? along === -1 : event.key === 'ArrowUp';
36
71
  const controls = this.controls;
37
- const current = controls.findIndex((entry) => entry.contains(event.target));
72
+ const origin = this.origin(event);
73
+ const current = origin ? controls.findIndex((control) => control.child === origin.child) : -1;
38
74
  if (current === -1) {
39
75
  return;
40
76
  }
@@ -57,30 +93,75 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
57
93
  event.preventDefault();
58
94
  const target = controls[next];
59
95
  this.setTabStop(target);
60
- target?.focus();
96
+ const element = target ? this.entry(target) : undefined;
97
+ if (element && 'focusItem' in element && typeof element.focusItem === 'function') {
98
+ element.focusItem();
99
+ }
100
+ else {
101
+ element?.focus();
102
+ }
61
103
  };
62
104
  }
63
105
  get controls() {
64
- return [...this.children].filter((child) => {
65
- if (!(child instanceof HTMLElement)) {
66
- return false;
106
+ const controls = [];
107
+ for (const child of this.children) {
108
+ if (!(child instanceof HTMLElement) ||
109
+ child.hasAttribute('disabled') ||
110
+ child.getAttribute('aria-hidden') === 'true') {
111
+ continue;
67
112
  }
68
- return !child.hasAttribute('disabled') && child.getAttribute('aria-hidden') !== 'true';
69
- });
113
+ const stops = takesFocus(child) ? this.inside(child) : this.within(child);
114
+ if (stops.length > 0) {
115
+ controls.push({ child, stops });
116
+ }
117
+ }
118
+ return controls;
119
+ }
120
+ within(child) {
121
+ return [...child.querySelectorAll('*')].filter(takesFocus);
122
+ }
123
+ inside(child) {
124
+ if (!ownsTabStop(child)) {
125
+ return [child];
126
+ }
127
+ const members = this.within(child);
128
+ return members.length > 1 ? members : [child];
129
+ }
130
+ entry(control) {
131
+ const { stops } = control;
132
+ if (stops.length === 1) {
133
+ return stops[0];
134
+ }
135
+ return (stops.find((stop) => (ownsTabStop(stop) && stop.focusable) ||
136
+ stop.selected === true) ?? stops[0]);
137
+ }
138
+ origin(event) {
139
+ const path = event.composedPath();
140
+ return this.controls.find((control) => control.stops.some((stop) => path.includes(stop)));
70
141
  }
71
142
  setTabStop(active, foreign = true) {
143
+ if (foreign) {
144
+ this.wroteForeign = true;
145
+ }
72
146
  for (const control of this.controls) {
73
- const stop = control === active;
74
- if (ownsTabStop(control)) {
75
- control.focusable = stop;
147
+ if (control.stops.length !== 1) {
148
+ if (ownsTabStop(control.child)) {
149
+ control.child.focusable = control.child === active?.child;
150
+ }
151
+ continue;
152
+ }
153
+ const element = control.stops[0];
154
+ const stop = control.child === active?.child;
155
+ if (ownsTabStop(element)) {
156
+ element.focusable = stop;
76
157
  }
77
- else if (foreign) {
78
- control.tabIndex = stop ? 0 : -1;
158
+ else if (foreign || this.wroteForeign) {
159
+ element.tabIndex = stop ? 0 : -1;
79
160
  }
80
161
  }
81
162
  }
82
163
  async claimTabStop() {
83
- const pending = [...new Set(this.controls.map((control) => control.localName))].filter((name) => name.includes('-'));
164
+ const pending = [...new Set([...this.children].map((child) => child.localName))].filter((name) => name.includes('-'));
84
165
  await Promise.all(pending.map((name) => customElements.whenDefined(name)));
85
166
  this.setTabStop(this.controls[0], false);
86
167
  }
@@ -89,6 +170,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
89
170
  }
90
171
  connectedCallback() {
91
172
  super.connectedCallback();
173
+ this.addEventListener('keydown', this.handleCompositeKeydown, true);
92
174
  this.addEventListener('keydown', this.handleKeydown);
93
175
  this.addEventListener('focusin', this.handleFocusIn);
94
176
  this.watcher = new MutationObserver(this.restoreTabStop);
@@ -100,6 +182,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
100
182
  }
101
183
  disconnectedCallback() {
102
184
  super.disconnectedCallback();
185
+ this.removeEventListener('keydown', this.handleCompositeKeydown, true);
103
186
  this.removeEventListener('keydown', this.handleKeydown);
104
187
  this.removeEventListener('focusin', this.handleFocusIn);
105
188
  this.watcher?.disconnect();
@@ -27,7 +27,12 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
27
27
  * `for`, the tooltip warns once on first open if the attribute is missing.
28
28
  *
29
29
  * The tooltip shows on hover and on focus. It hides on pointer leave, on blur,
30
- * on Escape, and when the trigger is activated.
30
+ * on Escape, and when the trigger is activated. The pointer can move from the
31
+ * trigger onto the bubble and the bubble stays.
32
+ *
33
+ * Once one tooltip is showing, moving to another trigger shows its tooltip
34
+ * with no `delay`, and the first one hides. The same holds for 300
35
+ * milliseconds after a tooltip hides.
31
36
  *
32
37
  * @since 0.1.0
33
38
  * @status stable
@@ -35,13 +40,15 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
35
40
  * @aliases Hint, Popup label
36
41
  * @documentation https://fusion.dev/helix/components/tooltip
37
42
  *
38
- * @slot - The trigger: the element the tooltip describes.
43
+ * @slot - The trigger, in the wrapping form. A trigger here gets no
44
+ * description, so use it only where the trigger's name says what the tooltip
45
+ * says, and use `for` otherwise.
39
46
  * @slot content - Rich content for the bubble. It replaces the `content`
40
47
  * property.
41
48
  *
42
49
  * @event {HelixBeforeCloseEvent} helix-before-close - The bubble is about to hide. Cancelable: cancel it to keep it shown.
43
50
  * @event {HelixBeforeOpenEvent} helix-before-open - The bubble is about to show. Cancelable: cancel it to keep it hidden.
44
- * @event {Event} toggle - The bubble opened or closed. The event carries `newState` and `oldState`.
51
+ * @event {ToggleEvent} toggle - The bubble opened or closed. The event carries `newState` and `oldState`.
45
52
  *
46
53
  * @csspart arrow - The arrow that points at the trigger.
47
54
  * @csspart arrow-bottom - Added to `arrow` while the bubble is below its trigger, with the arrow on the bubble's top edge.
@@ -249,8 +256,27 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
249
256
  * the reader had moved on to.
250
257
  */
251
258
  hide(): void;
252
- /** Shows after the pointer has rested for `delay`. */
259
+ /**
260
+ * Shows after the pointer has rested for `delay`, or at once while the
261
+ * tooltips are warm.
262
+ *
263
+ * **Warm is another tooltip showing, or one that hid within `SKIP_DELAY`.**
264
+ * A reader scanning a toolbar has already waited once, and waiting again on
265
+ * every control turns the scan into a series of pauses.
266
+ */
253
267
  private readonly showLater;
268
+ /**
269
+ * The pointer reached the bubble, in the `for` form.
270
+ *
271
+ * The bubble is not inside the element `for` names, so moving onto it leaves
272
+ * the trigger and would hide it after `delay-out`. WCAG 1.4.13 asks that a
273
+ * reader can move onto it, to read it under a magnifier or to select its
274
+ * text. Wrapping, the bubble is inside the host and the host's own pointer
275
+ * listeners already hold it.
276
+ */
277
+ private readonly holdOnBubble;
278
+ /** The pointer left the bubble, in the `for` form. */
279
+ private readonly leaveBubble;
254
280
  /** Hides after `delayOut`, so crossing the gap to the bubble does not close it. */
255
281
  private readonly hideLater;
256
282
  /**
@@ -342,6 +368,13 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
342
368
  * this should not be the thing to settle it.
343
369
  */
344
370
  private auditDescription;
371
+ /**
372
+ * Whether the wrapped trigger's own name is the tooltip's text.
373
+ *
374
+ * The name a consumer writes: `aria-label`, then a `label` a Helix control
375
+ * renders as its name, then the trigger's text.
376
+ */
377
+ private triggerSays;
345
378
  connectedCallback(): void;
346
379
  disconnectedCallback(): void;
347
380
  protected firstUpdated(changed: PropertyValues): void;
@@ -8,6 +8,13 @@ import { anchorElement, customElement, elementById, HelixElement, hostAria, Mixi
8
8
  import { html } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  const GENERIC = new Set(['div', 'p', 'section', 'span']);
11
+ const SKIP_DELAY = 300;
12
+ const WARMTH = Symbol.for('fusion.helix.tooltip.warmth');
13
+ const warmth = () => {
14
+ const globals = globalThis;
15
+ globals[WARMTH] ??= { hidAt: Number.NEGATIVE_INFINITY, showing: new Set() };
16
+ return globals[WARMTH];
17
+ };
11
18
  let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpen(HelixElement) {
12
19
  constructor() {
13
20
  super(...arguments);
@@ -51,8 +58,26 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
51
58
  this.showLater = () => {
52
59
  this.holds.add('pointer');
53
60
  clearTimeout(this.timer);
61
+ const warm = warmth();
62
+ const others = [...warm.showing].some((one) => one !== this);
63
+ if (others || performance.now() - warm.hidAt < SKIP_DELAY) {
64
+ this.show();
65
+ return;
66
+ }
54
67
  this.timer = setTimeout(() => this.show(), this.delay);
55
68
  };
69
+ this.holdOnBubble = () => {
70
+ if (!this.named) {
71
+ return;
72
+ }
73
+ this.holds.add('pointer');
74
+ clearTimeout(this.timer);
75
+ };
76
+ this.leaveBubble = () => {
77
+ if (this.named) {
78
+ this.hideLater();
79
+ }
80
+ };
56
81
  this.hideLater = () => {
57
82
  this.holds.delete('pointer');
58
83
  clearTimeout(this.timer);
@@ -175,6 +200,9 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
175
200
  }
176
201
  this.audited = true;
177
202
  if (this.for === '') {
203
+ if (this.triggerSays(this.describedText)) {
204
+ return;
205
+ }
178
206
  reportRuntime({
179
207
  kind: 'wrapped-trigger',
180
208
  message: `<${this.localName}> is wrapping its trigger rather than naming one, so its text ` +
@@ -211,6 +239,12 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
211
239
  tag: this.localName,
212
240
  });
213
241
  }
242
+ triggerSays(text) {
243
+ const flat = (words) => (words ?? '').replace(/\s+/gu, ' ').trim();
244
+ const trigger = this.trigger;
245
+ const name = trigger.getAttribute('aria-label') ?? trigger.getAttribute('label') ?? trigger.textContent;
246
+ return text !== '' && flat(name) === flat(text);
247
+ }
214
248
  connectedCallback() {
215
249
  super.connectedCallback();
216
250
  if (this.hasUpdated) {
@@ -222,6 +256,7 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
222
256
  this.unbind();
223
257
  this.holds.clear();
224
258
  clearTimeout(this.timer);
259
+ warmth().showing.delete(this);
225
260
  }
226
261
  firstUpdated(changed) {
227
262
  super.firstUpdated(changed);
@@ -233,11 +268,22 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
233
268
  this.bind();
234
269
  }
235
270
  if (changed.has('open')) {
271
+ const warm = warmth();
236
272
  if (this.open) {
273
+ for (const other of [...warm.showing]) {
274
+ if (other !== this) {
275
+ other.holds.clear();
276
+ other.hide();
277
+ }
278
+ }
279
+ warm.showing.add(this);
237
280
  this.bubbleLayer.show();
238
281
  this.auditDescription();
239
282
  }
240
283
  else {
284
+ if (warm.showing.delete(this)) {
285
+ warm.hidAt = performance.now();
286
+ }
241
287
  this.bubbleLayer.hide();
242
288
  }
243
289
  }
@@ -257,6 +303,8 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
257
303
  part="tooltip${side ? { bottom: ' tooltip-bottom', left: ' tooltip-left', right: ' tooltip-right', top: ' tooltip-top' }[side] : ''}"
258
304
  popover="manual"
259
305
  role="tooltip"
306
+ @pointerenter=${this.holdOnBubble}
307
+ @pointerleave=${this.leaveBubble}
260
308
  >
261
309
  <span
262
310
  part="arrow${side ? { bottom: ' arrow-bottom', left: ' arrow-left', right: ' arrow-right', top: ' arrow-top' }[side] : ''}"
@@ -1,6 +1,7 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { HelixHeadlessTourStop } from '../stop/tour-stop.js';
4
+ declare const HelixHeadlessTour_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
4
5
  /**
5
6
  * @summary A guided tour of an interface, one stop at a time.
6
7
  *
@@ -17,6 +18,16 @@ import type { HelixHeadlessTourStop } from '../stop/tour-stop.js';
17
18
  * The root measures the target's position in the browser. Nothing is measured
18
19
  * on the server, because a tour starts only on request.
19
20
  *
21
+ * `open`, `default-open`, `helix-before-open`, `helix-before-close` and
22
+ * `toggle` work as they do on `Dialog`. Setting `open` starts the run, and
23
+ * `skip()` and `requestOpen(false)` end it early.
24
+ *
25
+ * **Focus follows the run.** Opening moves focus to the first stop's panel,
26
+ * and moving on from a control in a stop moves it to the next one. Ending the
27
+ * tour puts focus back on the element that had it when the tour opened, unless
28
+ * you have moved it somewhere else on the page. A stop does not trap focus, so
29
+ * you can still `Tab` out to the element it describes.
30
+ *
20
31
  * @since 0.1.0
21
32
  * @status stable
22
33
  * @category overlays
@@ -25,15 +36,20 @@ import type { HelixHeadlessTourStop } from '../stop/tour-stop.js';
25
36
  *
26
37
  * @slot - The stops, in the order they are shown.
27
38
  *
39
+ * @event {HelixActivateEvent} helix-activate - Fired after the current stop changes, with the new index.
28
40
  * @event {HelixBeforeActivateEvent} helix-before-activate - Fired before the current stop changes, with the new index. Cancelable: call `preventDefault()` to stay on the current stop.
29
41
  * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the user ends the tour early. Cancelable: call `preventDefault()` to keep the tour open.
42
+ * @event {HelixBeforeOpenEvent} helix-before-open - Fired before `requestOpen(true)` starts the tour. Cancelable: call `preventDefault()` to keep it closed.
30
43
  * @event {HelixCompleteEvent} helix-complete - Fired when the last stop is
31
44
  * finished.
32
45
  * @event {HelixTourAskEvent} helix-tour-ask - Fired when a stop's control is pressed to move the tour. The stop sends it, and the tour handles it.
46
+ * @event {ToggleEvent} toggle - Sent when the tour starts or ends, with `newState` and `oldState`.
33
47
  *
34
48
  * @csspart spotlight - The box over the current target. It also draws the dimmed area around the target.
49
+ *
50
+ * @cssstate open - While the tour is running.
35
51
  */
36
- export declare class HelixHeadlessTour extends HelixElement {
52
+ export declare class HelixHeadlessTour extends HelixHeadlessTour_base {
37
53
  /**
38
54
  * Label for the control that goes back, passed to every stop.
39
55
  */
@@ -70,10 +86,6 @@ export declare class HelixHeadlessTour extends HelixElement {
70
86
  * Label for the control that goes on.
71
87
  */
72
88
  nextLabel: string;
73
- /**
74
- * Whether the tour is running.
75
- */
76
- open: boolean;
77
89
  /**
78
90
  * Label for the control that leaves the tour.
79
91
  */
@@ -121,6 +133,22 @@ export declare class HelixHeadlessTour extends HelixElement {
121
133
  disconnectedCallback(): void;
122
134
  protected willUpdate(changed: PropertyValues): void;
123
135
  protected updated(changed: PropertyValues): void;
136
+ /** Where focus was when the tour opened, to give it back when it ends. */
137
+ private returnFocus;
138
+ /** Whether `follow` has seen the first update. */
139
+ private followed;
140
+ /**
141
+ * Moves focus with the run.
142
+ *
143
+ * Opening takes it to the current stop. Moving takes it to the new stop when
144
+ * it was in the tour, which is where a reader pressing Next is, and leaves it
145
+ * where it is otherwise. Closing gives it back to where it was before, again
146
+ * only when it was in the tour: a reader who moved to the target has chosen
147
+ * where to be.
148
+ */
149
+ private follow;
150
+ /** Focuses the current stop's panel once the stop has shown it. */
151
+ private focusCurrent;
124
152
  /**
125
153
  * Tells each stop where it is in the run.
126
154
  *
@@ -143,3 +171,4 @@ declare global {
143
171
  'helix-headless-tour': HelixHeadlessTour;
144
172
  }
145
173
  }
174
+ export {};