@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
@@ -26,17 +26,34 @@ const size = (bytes, locale) => {
26
26
  return `${written} ${unit}`;
27
27
  };
28
28
  const kind = (file) => {
29
- const subtype = file.type.split('/')[1];
30
- if (subtype) {
31
- return subtype.replace(/^x-/, '').split('+')[0]?.toUpperCase() ?? 'File';
29
+ const extension = /\.([a-z0-9]{1,6})$/i.exec(file.name)?.[1];
30
+ if (extension) {
31
+ return extension.toUpperCase();
32
32
  }
33
- const extension = file.name.includes('.') ? file.name.split('.').pop() : '';
34
- return extension ? extension.toUpperCase() : 'File';
33
+ const subtype = file.type.split('/')[1]?.replace(/^x-/, '').split('+')[0] ?? '';
34
+ return /^[a-z0-9]{1,6}$/i.test(subtype) ? subtype.toUpperCase() : 'File';
35
+ };
36
+ const accepts = (file, accept) => {
37
+ const tokens = accept
38
+ .split(',')
39
+ .map((token) => token.trim().toLowerCase())
40
+ .filter(Boolean);
41
+ if (tokens.length === 0) {
42
+ return true;
43
+ }
44
+ const name = file.name.toLowerCase();
45
+ const type = file.type.toLowerCase();
46
+ return tokens.some((token) => token.startsWith('.')
47
+ ? name.endsWith(token)
48
+ : token.endsWith('/*')
49
+ ? type.startsWith(token.slice(0, -1))
50
+ : type === token);
35
51
  };
36
52
  let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(HelixFormElement) {
37
53
  constructor() {
38
54
  super(...arguments);
39
55
  this.aria = new AriaForwardController(this, {
56
+ named: () => this.labeled,
40
57
  target: () => this.partNamed('control'),
41
58
  });
42
59
  this.accept = '';
@@ -67,7 +84,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
67
84
  return;
68
85
  }
69
86
  event.preventDefault();
70
- const dropped = [...event.dataTransfer.files];
87
+ const dropped = [...event.dataTransfer.files].filter((file) => accepts(file, this.accept));
71
88
  if (dropped.length === 0) {
72
89
  return;
73
90
  }
@@ -75,9 +92,14 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
75
92
  };
76
93
  this.handleRemove = (event) => {
77
94
  const at = Number(event.currentTarget.dataset.at);
78
- if (Number.isFinite(at)) {
79
- this.removeAt(at);
95
+ if (!Number.isFinite(at)) {
96
+ return;
80
97
  }
98
+ this.removeAt(at);
99
+ this.updateComplete.then(() => {
100
+ const buttons = [...this.renderRoot.querySelectorAll('[part="remove"]')];
101
+ (buttons[Math.min(at, buttons.length - 1)] ?? this.control)?.focus();
102
+ });
81
103
  };
82
104
  }
83
105
  get files() {
@@ -152,6 +174,10 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
152
174
  this.committed = this.files;
153
175
  this.sync();
154
176
  }
177
+ updated(changed) {
178
+ super.updated(changed);
179
+ this.sync();
180
+ }
155
181
  focus(options) {
156
182
  this.control?.focus(options);
157
183
  }
@@ -151,6 +151,15 @@ export declare class HelixHeadlessFilterMenu extends HelixElement {
151
151
  * a filter is never left looking at a menu that says it took.
152
152
  */
153
153
  apply(field: string, values: readonly string[], operator?: string): boolean;
154
+ /**
155
+ * Keeps each choice row's tick where this element puts it.
156
+ *
157
+ * A choice row checks itself after `helix-before-toggle`, and this element
158
+ * already sets `checked` on every row from its own state. The request is
159
+ * refused, so a tick moves only when the choice is applied, and stopped, so
160
+ * it does not reach a page that never placed the row.
161
+ */
162
+ private readonly keepTick;
154
163
  /**
155
164
  * A row was chosen: a value to tick, or a comparison to use.
156
165
  *
@@ -17,6 +17,12 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
17
17
  this.label = 'Filter';
18
18
  this.picked = new Map();
19
19
  this.comparisons = new Map();
20
+ this.keepTick = (event) => {
21
+ if (event.target === event.currentTarget) {
22
+ event.preventDefault();
23
+ event.stopPropagation();
24
+ }
25
+ };
20
26
  this.handleSelect = (event) => {
21
27
  const row = event.composedPath()[0];
22
28
  if (!(row instanceof HTMLElement)) {
@@ -152,6 +158,7 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
152
158
  checkable
153
159
  exclusive
154
160
  ?checked=${operator.name === using?.name}
161
+ @helix-before-toggle=${this.keepTick}
155
162
  data-facet="operator"
156
163
  data-field=${name}
157
164
  data-value=${operator.name}
@@ -173,6 +180,7 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
173
180
  checkable
174
181
  ?exclusive=${using?.arity === 'one'}
175
182
  ?checked=${ticked.has(option.value)}
183
+ @helix-before-toggle=${this.keepTick}
176
184
  data-facet="value"
177
185
  data-field=${name}
178
186
  data-value=${option.value}
@@ -1,6 +1,6 @@
1
1
  import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import type { HelixHeadlessFilterField } from '../field/filter-field.js';
3
+ import { HelixHeadlessFilterField } from '../field/filter-field.js';
4
4
  declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
5
5
  /**
6
6
  * @summary A text field for filters, with the active filters shown in it as tokens.
@@ -69,6 +69,9 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
69
69
  * @documentation https://fusion.dev/helix/components/filter-input
70
70
  *
71
71
  * @slot end - Content after the text, such as a count or a control.
72
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
73
+ * @slot hint - Help under the control, for content richer than the `hint` text.
74
+ * @slot label - The label, for content richer than the `label` text.
72
75
  * @slot start - Content before the text, such as a search icon.
73
76
  * @slot suggestions - Suggestions for what to type next, as `Option`s and `Option.Group`s.
74
77
  * @slot tags - The active filters, as `FilterChip`s or `Tag`s.
@@ -81,7 +84,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
81
84
  * @event {HelixFacetEvent} helix-facet - Fired when a filter changes, whether it was typed, picked, or removed. Cancelable: call `preventDefault()` to leave the text and the chip as they were.
82
85
  * @event {HelixSelectEvent} helix-select - Fired by an option in `suggestions` when it is chosen, and bubbles out of the field. Cancelable: call `preventDefault()` to stop other listeners acting on it. The field takes no action on a choice; you decide what it does.
83
86
  * @event {Event} input - Fired when the text changes.
84
- * @event {Event} toggle - Fired when the suggestions panel opens or closes, with `newState` and `oldState`.
87
+ * @event {ToggleEvent} toggle - Fired when the suggestions panel opens or closes, with `newState` and `oldState`.
85
88
  *
86
89
  * @csspart clear - The button that empties the field.
87
90
  * @csspart control - The input itself.
@@ -8,7 +8,9 @@ import { AriaForwardController, customElement, drawIcon, HelixFacetEvent, HelixF
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
+ import { HelixHeadlessFilterField } from '../field/filter-field.js';
11
12
  import { readFilterText } from '../grammar.js';
13
+ import { HelixHeadlessFilterMenu } from '../menu/filter-menu.js';
12
14
  const asksToGo = (node) => typeof node?.dismiss === 'function';
13
15
  const isOption = (node) => 'active' in node && 'value' in node;
14
16
  let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
@@ -66,7 +68,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
66
68
  menu.apply(field, []);
67
69
  };
68
70
  this.handleClear = () => {
69
- this.requestValue('');
71
+ if (this.value !== '' && this.requestValue('')) {
72
+ this.announceChange();
73
+ }
70
74
  const control = this.control;
71
75
  if (control) {
72
76
  restoreValue(control, this.value);
@@ -192,12 +196,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
192
196
  this.control?.setSelectionRange(start, end, direction);
193
197
  }
194
198
  get fields() {
195
- return [
196
- ...this.querySelectorAll('helix-headless-filter-field, helix-filter-field'),
197
- ];
199
+ return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessFilterField);
198
200
  }
199
201
  menuFor(field) {
200
- for (const menu of this.querySelectorAll('helix-headless-filter-menu, helix-filter-menu')) {
202
+ for (const menu of this.querySelectorAll('*')) {
203
+ if (!(menu instanceof HelixHeadlessFilterMenu)) {
204
+ continue;
205
+ }
201
206
  if (menu.fields.some((declared) => (declared.name ?? '') === field)) {
202
207
  return menu;
203
208
  }
@@ -264,7 +269,10 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
264
269
  this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
265
270
  }
266
271
  removeLastToken() {
267
- const last = this.tokens?.assignedElements({ flatten: true }).at(-1);
272
+ const last = this.tokens
273
+ ?.assignedElements({ flatten: true })
274
+ .filter((token) => !token.hidden)
275
+ .at(-1);
268
276
  if (asksToGo(last)) {
269
277
  last.dismiss();
270
278
  }
@@ -373,7 +381,14 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
373
381
  built only once there were options could never learn that there are
374
382
  any. A manual popover puts it in the top layer, so it is not clipped by
375
383
  a scrolling table or an overflow on an ancestor. -->
376
- <div part="surface" id="suggestions" role="listbox" popover="manual" ?hidden=${!this.open}>
384
+ <div
385
+ part="surface"
386
+ id="suggestions"
387
+ role="listbox"
388
+ aria-labelledby=${this.labeled ? this.labelId : nothing}
389
+ popover="manual"
390
+ ?hidden=${!this.open}
391
+ >
377
392
  <slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
378
393
  </div>
379
394
  ${this.renderHint()}${this.renderFeedback()}`;
@@ -16,9 +16,15 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
16
16
  * schedules an update for when its current reading expires, and an element
17
17
  * showing a date schedules none, so a long feed left open costs almost nothing.
18
18
  *
19
- * **The output is always `<time datetime>`**, so screen readers, copy and
20
- * paste, and scripts parsing the page get the exact instant, whatever the
21
- * visible text says.
19
+ * **The output is always `<time datetime>`**, so copy and paste and scripts
20
+ * parsing the page get the exact instant, whatever the visible text says. A
21
+ * screen reader reads the text, so an `elapsed` reading is named in full, such
22
+ * as "3 hours ago", and carries the whole date as its title.
23
+ *
24
+ * **A date with no time, such as `2026-08-04`, is that day everywhere.** It is
25
+ * formatted in UTC unless `time-zone` says otherwise, as Date Input and Date
26
+ * Picker format their dates, so a reader west of UTC sees August 4 and not
27
+ * August 3.
22
28
  *
23
29
  * @since 0.1.0
24
30
  * @status stable
@@ -114,6 +120,8 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
114
120
  private written;
115
121
  private stopTicking;
116
122
  private watching;
123
+ /** Whether the timestamp is a day with no time, which is formatted in UTC. */
124
+ private get dateOnly();
117
125
  /** The timestamp, or nothing where there is not one to read. */
118
126
  private get moment();
119
127
  /** Every `Intl.DateTimeFormat` option a consumer set, and none they did not. */
@@ -4,9 +4,10 @@ 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, elapsedTime, HelixElement, subscribe } from '@fusion.dev/helix-core';
7
+ import { customElement, elapsedTime, HelixElement, relativeTime, subscribe, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
10
11
  let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement {
11
12
  constructor() {
12
13
  super(...arguments);
@@ -18,6 +19,9 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
18
19
  this.stopTicking = undefined;
19
20
  this.watching = undefined;
20
21
  }
22
+ get dateOnly() {
23
+ return DATE_ONLY.test((this.date || this.written).trim());
24
+ }
21
25
  get moment() {
22
26
  const source = this.date || this.written;
23
27
  if (source === '') {
@@ -46,6 +50,9 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
46
50
  if (this.timeZone) {
47
51
  asked.timeZone = this.timeZone;
48
52
  }
53
+ else if (this.dateOnly) {
54
+ asked.timeZone = 'UTC';
55
+ }
49
56
  if (this.timeZoneName) {
50
57
  asked.timeZoneName = this.timeZoneName;
51
58
  }
@@ -120,10 +127,24 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
120
127
  if (!moment) {
121
128
  return nothing;
122
129
  }
123
- const text = this.elapsed
124
- ? elapsedTime(this.locale)(moment, new Date(this.clock ?? Date.now())).text
125
- : new Intl.DateTimeFormat(this.locale || undefined, this.options).format(moment);
126
- return html `<time part="time" datetime=${moment.toISOString()}>${text}</time>`;
130
+ const datetime = this.dateOnly ? (this.date || this.written).trim() : moment.toISOString();
131
+ if (!this.elapsed) {
132
+ const text = new Intl.DateTimeFormat(this.locale || undefined, this.options).format(moment);
133
+ return html `<time part="time" datetime=${datetime}>${text}</time>`;
134
+ }
135
+ const now = new Date(this.clock ?? Date.now());
136
+ const zone = this.timeZone || (this.dateOnly ? 'UTC' : undefined);
137
+ const whole = new Intl.DateTimeFormat(this.locale || undefined, {
138
+ dateStyle: 'long',
139
+ ...(this.dateOnly ? {} : { timeStyle: 'short' }),
140
+ ...(zone ? { timeZone: zone } : {}),
141
+ }).format(moment);
142
+ return html `<time
143
+ part="time"
144
+ datetime=${datetime}
145
+ aria-label=${relativeTime(this.locale)(moment, now).text}
146
+ title=${whole}
147
+ >${elapsedTime(this.locale)(moment, now).text}</time>`;
127
148
  }
128
149
  };
129
150
  __decorate([
@@ -232,16 +232,37 @@ export declare class HelixHeadlessGraph extends HelixHeadlessGraph_base {
232
232
  /**
233
233
  * Moves the keyboard to a row, and the list to show it.
234
234
  *
235
- * **Scrolled by the row's own box where it is drawn, and by the estimate
236
- * where it is not**, so a jump to the end of ten thousand notes lands there
237
- * and the row is drawn by the update the scroll asks for.
235
+ * **The row the keys are on is always drawn**, beside the window where it is
236
+ * outside it, so `aria-activedescendant` names a row that exists. The list
237
+ * then scrolls to it, see `revealCursor`.
238
238
  */
239
239
  private moveTo;
240
+ /**
241
+ * Brings the cursor's row into the list's box.
242
+ *
243
+ * **By the row's own box once it is in the window, and by the window
244
+ * otherwise.** The offset of a row that has not been drawn is a guess at its
245
+ * height, and a guess at 32 pixels was where End and Page Down sent the list
246
+ * when rows were taller: the list landed far from the row, which then was
247
+ * not drawn. So the window is moved toward the row, the rows it draws are
248
+ * measured, and the next try starts from there, a few times at most.
249
+ */
250
+ private revealCursor;
251
+ /** The cursor's row, where it is outside the window and drawn beside it. */
252
+ private get keptRow();
240
253
  /**
241
254
  * The keys in the list. **Kept from the frame**, whose arrows pan the
242
255
  * drawing: a reader moving through the list was moving the graph under it.
243
256
  */
244
257
  private readonly handleListKeydown;
258
+ /** What has been typed toward a note's name, and when the last letter came. */
259
+ private typed;
260
+ /**
261
+ * Moves to the next note whose name starts with what was typed, as a
262
+ * listbox does. Letters within half a second of each other run together,
263
+ * and a repeated letter steps through the names that start with it.
264
+ */
265
+ private typeahead;
245
266
  /**
246
267
  * The search field's keys, which belong to the field and never pan the
247
268
  * graph. Escape empties a search that has words in it, and passes through
@@ -4,10 +4,11 @@ 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, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, MixinControllerActive, MixinViewport, VirtualizeController, } from '@fusion.dev/helix-core';
7
+ import { customElement, fillTemplate, formatNumbers, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, MixinControllerActive, MixinViewport, scrollItemIntoView, VirtualizeController, } from '@fusion.dev/helix-core';
8
8
  import { list, magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
+ import { repeat } from 'lit/directives/repeat.js';
11
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
13
  import { adjacencyOf, entriesOf, groupsOf, nameOf, sortedOf, within, withoutGroups, } from './graph-model.js';
13
14
  const PAGE = 10;
@@ -65,8 +66,15 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
65
66
  event.preventDefault();
66
67
  event.stopPropagation();
67
68
  this.choose(this.cursor);
69
+ return;
70
+ }
71
+ if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ this.typeahead(event.key);
68
75
  }
69
76
  };
77
+ this.typed = { at: 0, text: '' };
70
78
  this.handleSearchKeydown = (event) => {
71
79
  if (event.key === 'ArrowDown') {
72
80
  event.preventDefault();
@@ -236,18 +244,50 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
236
244
  moveTo(index) {
237
245
  const last = this.entries.length - 1;
238
246
  this.cursor = Math.min(Math.max(index, 0), Math.max(last, 0));
247
+ void this.updateComplete.then(() => this.revealCursor());
248
+ }
249
+ revealCursor(attempts = 8) {
239
250
  const list = this.partNamed('list');
240
251
  if (!list) {
241
252
  return;
242
253
  }
243
- const row = this.renderRoot.querySelector?.(`[data-index="${this.cursor}"]`);
244
- if (row) {
245
- row.scrollIntoView?.({ block: 'nearest' });
254
+ const { first, last } = this.virtualize.window;
255
+ if (this.cursor >= first && this.cursor < last) {
256
+ const row = this.renderRoot.querySelector?.(`[data-index="${this.cursor}"]`);
257
+ if (row) {
258
+ scrollItemIntoView(row, { scroller: list });
259
+ }
260
+ return;
246
261
  }
247
- else {
248
- list.scrollTop = this.cursor * 32;
262
+ if (attempts === 0) {
263
+ return;
249
264
  }
265
+ this.virtualize.reveal(this.cursor);
250
266
  this.virtualize.measure();
267
+ void this.updateComplete.then(() => this.revealCursor(attempts - 1));
268
+ }
269
+ get keptRow() {
270
+ const { first, last } = this.virtualize.window;
271
+ const cursor = this.cursor;
272
+ return this.entries.length > 0 && (cursor < first || cursor >= last) ? cursor : undefined;
273
+ }
274
+ typeahead(letter) {
275
+ const now = Date.now();
276
+ const text = now - this.typed.at < 500 ? this.typed.text + letter : letter;
277
+ this.typed = { at: now, text };
278
+ const wanted = text.toLocaleLowerCase();
279
+ const repeated = [...wanted].every((one) => one === wanted[0]);
280
+ const prefix = repeated ? (wanted[0] ?? '') : wanted;
281
+ const entries = this.entries;
282
+ const start = repeated ? this.cursor + 1 : this.cursor;
283
+ for (let step = 0; step < entries.length; step += 1) {
284
+ const at = (start + step) % entries.length;
285
+ const entry = entries[at];
286
+ if (entry && nameOf(entry.node).toLocaleLowerCase().startsWith(prefix)) {
287
+ this.moveTo(at);
288
+ return;
289
+ }
290
+ }
251
291
  }
252
292
  togglePanel(open = !this.panelOpen) {
253
293
  const inside = this.panelOpen && this.partNamed('panel')?.contains(this.shadowRoot?.activeElement ?? null);
@@ -290,8 +330,18 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
290
330
  renderRows() {
291
331
  const { first, last } = this.virtualize.window;
292
332
  const total = this.entries.length;
293
- return this.entries.slice(first, last).map((entry, offset) => {
294
- const index = first + offset;
333
+ const indices = Array.from({ length: Math.max(0, Math.min(last, total) - first) }, (_, at) => first + at);
334
+ const kept = this.keptRow;
335
+ if (kept !== undefined && kept < total) {
336
+ if (kept < first) {
337
+ indices.unshift(kept);
338
+ }
339
+ else {
340
+ indices.push(kept);
341
+ }
342
+ }
343
+ return html `${repeat(indices, (index) => index, (index) => {
344
+ const entry = this.entries[index];
295
345
  const relation = this.relationText(entry);
296
346
  const chosen = entry.relation === 'selected';
297
347
  return html `<div
@@ -311,10 +361,14 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
311
361
  <span part="note-name">${nameOf(entry.node)}</span>
312
362
  ${relation ? html `<span part="note-relation" aria-hidden="true">${relation}</span>` : nothing}
313
363
  </div>`;
314
- });
364
+ })}`;
315
365
  }
316
366
  render() {
317
- const { after, before } = this.virtualize.window;
367
+ const window = this.virtualize.window;
368
+ const kept = this.keptRow;
369
+ const room = kept === undefined ? 0 : this.virtualize.sizeOf(kept);
370
+ const before = Math.max(0, window.before - (kept !== undefined && kept < window.first ? room : 0));
371
+ const after = Math.max(0, window.after - (kept !== undefined && kept >= window.last ? room : 0));
318
372
  const cursor = this.entries.length > 0 ? `note-${this.cursor}` : nothing;
319
373
  return html `<div
320
374
  part="region"
@@ -368,9 +422,9 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
368
422
  @keydown=${this.handleListKeydown}
369
423
  @scroll=${() => this.virtualize.measure()}
370
424
  >
371
- <div part="before" style="block-size: ${before}px"></div>
425
+ <div part="before" role="presentation" style="block-size: ${before}px"></div>
372
426
  ${this.renderRows()}
373
- <div part="after" style="block-size: ${after}px"></div>
427
+ <div part="after" role="presentation" style="block-size: ${after}px"></div>
374
428
  </div>
375
429
  </div>
376
430
  </div>
@@ -10,7 +10,8 @@
10
10
  /** A reading, reduced to what the bucketing needs. */
11
11
  export interface Reading {
12
12
  tone?: string | undefined;
13
- value: number;
13
+ /** The number, or undefined for a row whose cell was blank. */
14
+ value: number | undefined;
14
15
  }
15
16
  /**
16
17
  * The tone for one reading, from its own `data-tone` or from the thresholds.
@@ -6,8 +6,12 @@ export const toneOf = (reading, thresholds, tones) => {
6
6
  return undefined;
7
7
  }
8
8
  let chosen;
9
+ const value = reading.value;
10
+ if (value === undefined) {
11
+ return undefined;
12
+ }
9
13
  for (const [index, bound] of thresholds.entries()) {
10
- if (reading.value >= bound) {
14
+ if (value >= bound) {
11
15
  chosen = tones[index] ?? chosen;
12
16
  }
13
17
  }
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../tooltip/index.js';
4
4
  declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface);
@@ -44,7 +44,7 @@ declare const HelixHeadlessHeatmap_base: typeof HelixElement & (new (...args: an
44
44
  * @csspart footer - The three-slot row under the strip.
45
45
  * @csspart mark - The invisible point the tooltip is anchored to.
46
46
  * @csspart start - The leading slot's box.
47
- * @csspart strip - The grid of cells.
47
+ * @csspart strip - The grid of cells. It is an image, named by the host's `aria-labelledby` or `aria-label`, then by `label`, then by the caption of the table.
48
48
  * @csspart summary - The middle slot's box.
49
49
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
50
50
  * @csspart tooltip-anchor - The `Tooltip` element itself.
@@ -188,14 +188,37 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
188
188
  * none.
189
189
  */
190
190
  private hovered;
191
+ /**
192
+ * Carries a host `aria-labelledby` and `aria-describedby` to the strip, the
193
+ * image a reader lands on. The host is a generic around it.
194
+ */
195
+ protected readonly aria: AriaForwardController;
196
+ /**
197
+ * Hears an `aria-label` written on the element, which no property mirrors.
198
+ *
199
+ * Lit observes the attributes of its own properties only, and `ariaLabel`
200
+ * belongs to the platform.
201
+ */
202
+ static get observedAttributes(): string[];
203
+ /**
204
+ * Called by the browser when an observed attribute changes. A new
205
+ * `aria-label` asks for an update, because it names the strip.
206
+ */
207
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
191
208
  private observer;
192
209
  private announced;
193
210
  /**
194
- * Every reading in the table, oldest first, as the table is written.
211
+ * Every row in the table, oldest first, as the table is written, with its
212
+ * reading where the row has one.
195
213
  *
196
214
  * **Not `readings`, which the mixin already has and which means a count.**
197
215
  * Two members of the same name meaning a number and an array is the kind of
198
216
  * collision that type-checks in one direction and reads wrong in both.
217
+ *
218
+ * **One cell per row, and a blank row is a cell with no reading.** Taken from
219
+ * the series alone, a blank dropped its day and every day after it moved one
220
+ * cell along, while the tooltip still counted rows and named the day beside
221
+ * the one under the pointer.
199
222
  */
200
223
  private get points();
201
224
  /**
@@ -262,7 +285,6 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
262
285
  private get drawn();
263
286
  connectedCallback(): void;
264
287
  disconnectedCallback(): void;
265
- protected willUpdate(changed: PropertyValues): void;
266
288
  /**
267
289
  * Says on the footer whether any of its three slots holds anything.
268
290
  *
@@ -303,8 +325,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
303
325
  *
304
326
  * `pointerover` on the strip rather than a listener per cell: the event
305
327
  * bubbles, so one handler covers however many cells there are, and unlike
306
- * `mouseover` it is delivered for a pen and for the touch that follows a long
307
- * press.
328
+ * `mouseover` it is delivered for a pen.
308
329
  */
309
330
  private readonly onPointerOver;
310
331
  private readonly onPointerLeave;
@@ -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 { CHART_PLOT, customElement, drawTooltip, HelixElement, HelixResizeEvent, hostAria, MixinChartSource, markRegion, searchableById, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, byRow, CHART_PLOT, customElement, drawTooltip, HelixElement, HelixResizeEvent, MixinChartSource, markRegion, searchableById, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
@@ -21,9 +21,15 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
21
21
  this.width = undefined;
22
22
  this.metrics = undefined;
23
23
  this.hovered = -1;
24
+ this.aria = new AriaForwardController(this, {
25
+ target: () => this.partNamed('strip'),
26
+ });
24
27
  this.observer = undefined;
25
28
  this.announced = undefined;
26
29
  this.onPointerOver = (event) => {
30
+ if (!this.pointer.accepts(event)) {
31
+ return;
32
+ }
27
33
  const cell = event.target?.closest?.('[part="cell"]');
28
34
  const index = Number(cell?.getAttribute('data-index') ?? -1);
29
35
  this.hovered = Number.isInteger(index) ? index : -1;
@@ -35,8 +41,21 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
35
41
  get visible() {
36
42
  return Math.min(this.points.length, this.room);
37
43
  }
44
+ static get observedAttributes() {
45
+ return [...super.observedAttributes, 'aria-label'];
46
+ }
47
+ attributeChangedCallback(name, old, value) {
48
+ super.attributeChangedCallback(name, old, value);
49
+ if (name === 'aria-label') {
50
+ this.requestUpdate();
51
+ }
52
+ }
38
53
  get points() {
39
- return this.series[0]?.points ?? [];
54
+ const series = this.series[0];
55
+ if (!series) {
56
+ return [];
57
+ }
58
+ return byRow(series, this.sourceRows.length).map((point) => point ?? { value: undefined });
40
59
  }
41
60
  get sourceKnown() {
42
61
  if (this.for === '') {
@@ -96,10 +115,6 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
96
115
  this.observer?.disconnect();
97
116
  this.observer = undefined;
98
117
  }
99
- willUpdate(changed) {
100
- super.willUpdate(changed);
101
- hostAria(this.internals, { label: this.label || 'Heatmap', role: 'img' });
102
- }
103
118
  markEmpty() {
104
119
  markRegion(this.renderRoot, 'footer', ['start', 'summary', 'end']);
105
120
  }
@@ -127,8 +142,10 @@ let HelixHeadlessHeatmap = class HelixHeadlessHeatmap extends MixinChartSource(H
127
142
  const palette = writtenList(this.tones);
128
143
  const cells = this.drawn;
129
144
  return html `<div
145
+ aria-label=${this.getAttribute?.('aria-label') || this.named || 'Heatmap'}
130
146
  part="strip"
131
147
  id=${CHART_PLOT}
148
+ role="img"
132
149
  style="--heatmap-rows: ${this.rowCount}"
133
150
  @pointerover=${this.onPointerOver}
134
151
  @pointerleave=${this.onPointerLeave}