@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
@@ -4,10 +4,23 @@ 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 } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, reportRuntime } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessStat = class HelixHeadlessStat extends HelixElement {
11
+ updated(changed) {
12
+ super.updated(changed);
13
+ const given = this.direction;
14
+ if (changed.has('direction') && (given === 'up' || given === 'down' || given === 'neutral')) {
15
+ reportRuntime({
16
+ kind: 'misread-direction',
17
+ message: `<${this.localName} direction="${given}"> lays out as the column: \`${given}\` is a ` +
18
+ "direction of Trend's, which says which way a number moved. Put it on the " +
19
+ '`helix-trend` beside the stat.',
20
+ tag: this.localName,
21
+ });
22
+ }
23
+ }
11
24
  render() {
12
25
  return html `<slot name="label"></slot>
13
26
  <slot></slot>
@@ -12,7 +12,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
12
12
  super(...arguments);
13
13
  this.label = 'Steps';
14
14
  this.handleSelection = (event) => {
15
- if (event.defaultPrevented) {
15
+ if (event.defaultPrevented || !this.steps.includes(event.target)) {
16
16
  return;
17
17
  }
18
18
  this.requestActive(event.detail.value);
@@ -25,6 +25,8 @@ declare const HelixHeadlessSwitch_base: (new (...args: any[]) => import("@fusion
25
25
  * @documentation https://fusion.dev/helix/components/switch
26
26
  *
27
27
  * @slot - The label that names what the switch turns on.
28
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
29
+ * @slot hint - Help under the control, for content richer than the `hint` text.
28
30
  *
29
31
  * @event {Event} change - Sent from the input after the state changes.
30
32
  * @event {HelixBeforeToggleEvent} helix-before-toggle - Fired before the state changes, and cancelable.
@@ -11,6 +11,7 @@ let HelixHeadlessSwitch = class HelixHeadlessSwitch extends MixinControllerCheck
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.aria = new AriaForwardController(this, {
14
+ label: true,
14
15
  target: () => this.partNamed('control'),
15
16
  });
16
17
  this.clicks = new HostClickController(this, () => this.partNamed('control'));
@@ -5,9 +5,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
5
5
  *
6
6
  * @description
7
7
  * Sets its own role, because a grid of elements has no table structure for
8
- * assistive technology otherwise. `header` makes the cell a `columnheader`, so
9
- * a screen reader announces the heading with the value, for example "Attribute,
10
- * appearance".
8
+ * assistive technology otherwise. `header` makes the cell a header, so a screen
9
+ * reader announces it with the values it heads:
10
+ *
11
+ * - In a row that holds a cell without `header`, it is a `rowheader` and names
12
+ * its row, for example "web-app" in a row of a service's values.
13
+ * - In a row of `header` cells alone, it is a `columnheader`, which is what a
14
+ * table with no `TableColumn` heads its columns with.
11
15
  *
12
16
  * The role is on the host so the cell stays a child of its row in the flat
13
17
  * tree, which keeps the table structure intact.
@@ -25,10 +29,26 @@ import { type PropertyValues, type TemplateResult } from 'lit';
25
29
  */
26
30
  export declare class HelixHeadlessTableCell extends HelixElement {
27
31
  /**
28
- * Whether the cell is the header cell of its column.
32
+ * Whether the cell is a header: of its row in a row of values, and of its
33
+ * column in a row of headers alone.
29
34
  */
30
35
  header: boolean;
36
+ /**
37
+ * @internal Read by the row and by the cells beside it, to tell a header
38
+ * cell from a value. Not a consumer's to read or set.
39
+ */
40
+ readonly isTableCell = true;
41
+ /**
42
+ * Whether this cell, as a header, heads its row: true where the row also
43
+ * holds a cell that is not a header.
44
+ *
45
+ * The rule a browser applies to a `<th>` with no `scope`. Read from the
46
+ * siblings, which a server does not have, so a server render waits for the
47
+ * client to say which.
48
+ */
49
+ private get headsRow();
31
50
  protected willUpdate(changed: PropertyValues): void;
51
+ protected updated(changed: PropertyValues): void;
32
52
  /**
33
53
  * Sets the `text` state from the cell's own content.
34
54
  *
@@ -11,14 +11,34 @@ let HelixHeadlessTableCell = class HelixHeadlessTableCell extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.header = false;
14
+ this.isTableCell = true;
14
15
  this.handleSlotChange = () => {
15
16
  const text = [...this.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '');
16
17
  this.toggleState('text', text);
17
18
  };
18
19
  }
20
+ get headsRow() {
21
+ const siblings = [...(this.parentElement?.children ?? [])];
22
+ return siblings.some((sibling) => sibling !== this &&
23
+ 'isTableCell' in sibling &&
24
+ !sibling.hasAttribute('header') &&
25
+ sibling.header !== true);
26
+ }
19
27
  willUpdate(changed) {
20
28
  super.willUpdate(changed);
21
- hostAria(this.internals, { role: this.header ? 'columnheader' : 'cell' });
29
+ const header = this.headsRow ? 'rowheader' : 'columnheader';
30
+ hostAria(this.internals, { role: this.header ? header : 'cell' });
31
+ }
32
+ updated(changed) {
33
+ super.updated(changed);
34
+ if (changed.has('header') && changed.get('header') !== undefined) {
35
+ for (const sibling of this.parentElement?.children ?? []) {
36
+ if (sibling !== this && 'isTableCell' in sibling) {
37
+ ;
38
+ sibling.requestUpdate();
39
+ }
40
+ }
41
+ }
22
42
  }
23
43
  render() {
24
44
  return html `<slot @slotchange=${this.handleSlotChange}></slot>`;
@@ -68,12 +68,13 @@ export declare const edgeOf: (align: string | undefined) => string | undefined;
68
68
  * them with the column: a row with more cells than the table has tracks wraps
69
69
  * onto the next line.
70
70
  *
71
- * A heading that does something holds a control. A column header is not
72
- * focusable, so a sortable heading contains a `<button>`, which is the pattern
73
- * for a sortable table and the only way to reach the sort by keyboard. A
74
- * resizable column has a `separator` on its end edge, moved by dragging or with
75
- * the arrow keys. In a table with reorderable columns, each column has a grip
76
- * that the table moves.
71
+ * A heading that does something holds a control. In a `Table` a column
72
+ * header is not focusable, so a sortable heading contains a `<button>`, which
73
+ * is the pattern for a sortable table and the only way to reach the sort by
74
+ * keyboard. In a `DataTable` the heading is a cell of the grid, reached with
75
+ * the arrow keys, and its controls with Enter or F2. A resizable column has a
76
+ * `separator` on its end edge, moved by dragging or with the arrow keys. In a
77
+ * table with reorderable columns, each column has a grip that the table moves.
77
78
  *
78
79
  * @since 0.1.0
79
80
  * @status stable
@@ -254,6 +255,21 @@ export declare class HelixHeadlessTableColumn extends HelixElement {
254
255
  * on a column the table will not move is a control that does nothing.
255
256
  */
256
257
  movable: boolean;
258
+ /**
259
+ * @internal Whether the column is a cell of a grid that moves focus between
260
+ * its cells, written by that table. Not a consumer's to set.
261
+ *
262
+ * **The heading's box takes the role and the focus.** The column element has
263
+ * no box, being `display: contents`, and an element with no box cannot be
264
+ * focused, so in a grid the `columnheader` role, `aria-sort` and the focus all
265
+ * go to `[part="column"]`, and the controls on it leave the tab order.
266
+ */
267
+ gridCell: boolean;
268
+ /**
269
+ * @internal Whether the heading is the grid's one tab stop, written by the
270
+ * table that sets `gridCell`. Not a consumer's to set.
271
+ */
272
+ tabStop: boolean;
257
273
  /** How wide the heading drew, for the separator to report. */
258
274
  private drawn;
259
275
  /** Where a drag of the separator started, while one is running. */
@@ -300,6 +316,16 @@ export declare class HelixHeadlessTableColumn extends HelixElement {
300
316
  */
301
317
  order(row: Readonly<Record<string, unknown>>): unknown;
302
318
  protected willUpdate(changed: PropertyValues): void;
319
+ /**
320
+ * @internal The box a grid focuses for this heading, read by the table that
321
+ * sets `gridCell`. Not a consumer's to call.
322
+ */
323
+ get cellBox(): HTMLElement | null;
324
+ /**
325
+ * @internal The controls on the heading, in order, read by the table that
326
+ * sets `gridCell` to move focus into them. Not a consumer's to call.
327
+ */
328
+ get controls(): HTMLElement[];
303
329
  /** The words of the heading, for naming the controls on it. */
304
330
  private get name();
305
331
  connectedCallback(): void;
@@ -43,6 +43,8 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
43
43
  this.isTableColumn = true;
44
44
  this.sorted = undefined;
45
45
  this.movable = false;
46
+ this.gridCell = false;
47
+ this.tabStop = false;
46
48
  this.drawn = 0;
47
49
  this.handleResizeStart = (event) => {
48
50
  if (event.button !== 0) {
@@ -101,11 +103,25 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
101
103
  }
102
104
  willUpdate(changed) {
103
105
  super.willUpdate(changed);
104
- hostAria(this.internals, { role: 'columnheader' });
105
- this.internals.ariaSort = this.canSort ? (this.sorted ?? 'none') : null;
106
+ const named = this.movable || this.resizable;
107
+ const host = !this.gridCell;
108
+ hostAria(this.internals, {
109
+ label: host && named ? this.name : '',
110
+ role: host ? 'columnheader' : '',
111
+ });
112
+ this.internals.ariaSort = host && this.canSort ? (this.sorted ?? 'none') : null;
106
113
  this.toggleState('sorted-ascending', this.sorted === 'ascending');
107
114
  this.toggleState('sorted-descending', this.sorted === 'descending');
108
115
  }
116
+ get cellBox() {
117
+ return this.partNamed('column');
118
+ }
119
+ get controls() {
120
+ return ['grip', 'sort', 'resizer'].flatMap((name) => {
121
+ const control = this.partNamed(name);
122
+ return control ? [control] : [];
123
+ });
124
+ }
109
125
  get name() {
110
126
  return this.textContent?.trim() ?? '';
111
127
  }
@@ -167,19 +183,27 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
167
183
  }
168
184
  render() {
169
185
  const name = this.name;
170
- return html `<span part="column">${this.movable
171
- ? html `<button part="grip" type="button" aria-label=${fillTemplate(this.moveLabel, { name: name || 'column' })}>
186
+ const grid = this.gridCell;
187
+ const control = grid ? '-1' : nothing;
188
+ return html `<span
189
+ part="column"
190
+ role=${grid ? 'columnheader' : nothing}
191
+ tabindex=${grid ? (this.tabStop ? '0' : '-1') : nothing}
192
+ aria-sort=${grid && this.canSort ? (this.sorted ?? 'none') : nothing}
193
+ aria-label=${grid && (this.movable || this.resizable) && name ? name : nothing}
194
+ >${this.movable
195
+ ? html `<button part="grip" type="button" tabindex=${control} aria-label=${fillTemplate(this.moveLabel, { name: name || 'column' })}>
172
196
  <svg aria-hidden="true" fill="currentColor" part="grip-mark" viewBox=${dotsThreeVertical.viewBox}>
173
197
  ${unsafeSVG(dotsThreeVertical.content)}
174
198
  </svg>
175
199
  </button>`
176
200
  : nothing}${this.canSort
177
- ? html `<button part="sort" type="button"><slot></slot></button>`
201
+ ? html `<button part="sort" type="button" tabindex=${control}><slot></slot></button>`
178
202
  : html `<slot></slot>`}${this.resizable
179
203
  ? html `<span
180
204
  part="resizer"
181
205
  role="separator"
182
- tabindex="0"
206
+ tabindex=${grid ? '-1' : '0'}
183
207
  aria-label=${fillTemplate(this.resizeLabel, { name: name || 'column' })}
184
208
  aria-orientation="vertical"
185
209
  aria-valuemin=${HelixHeadlessTableColumn_1.NARROWEST}
@@ -231,6 +255,12 @@ __decorate([
231
255
  __decorate([
232
256
  state()
233
257
  ], HelixHeadlessTableColumn.prototype, "movable", void 0);
258
+ __decorate([
259
+ state()
260
+ ], HelixHeadlessTableColumn.prototype, "gridCell", void 0);
261
+ __decorate([
262
+ state()
263
+ ], HelixHeadlessTableColumn.prototype, "tabStop", void 0);
234
264
  __decorate([
235
265
  state()
236
266
  ], HelixHeadlessTableColumn.prototype, "drawn", void 0);
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import { type HelixHeadlessTableColumn } from '../column/table-column.js';
4
4
  declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & typeof HelixElement;
@@ -12,8 +12,14 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
12
12
  * else.
13
13
  *
14
14
  * A grid of elements has no table structure for assistive technology, so the
15
- * roles are set explicitly: `table`, `row`, `cell`, and `columnheader`. Rows
16
- * are `display: contents`, so their cells become items of the one grid.
15
+ * roles are set explicitly: `table`, `row`, `cell`, `columnheader`, and
16
+ * `rowheader` for a `header` cell in a row of values. Rows are
17
+ * `display: contents`, so their cells become items of the one grid.
18
+ *
19
+ * A column with `sortable` and a `field` sorts the rows by the text of its
20
+ * cells, numbers by their value. Your markup is not reordered: the table draws
21
+ * the rows in the sorted order. `helix-sort` asks first, so you can refuse it
22
+ * and reorder the rows yourself.
17
23
  *
18
24
  * The CSS custom properties listed for this component are written by it and
19
25
  * read by the stylesheet that draws it. Do not set them. Each holds a number
@@ -28,6 +34,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
28
34
  *
29
35
  * @slot - The table's rows.
30
36
  *
37
+ * @event {HelixSortEvent} helix-sort - A different sort order was requested, as `field:direction`, or empty for the order the rows were written in. Cancelable: cancel it to keep the current order.
38
+ *
31
39
  * @csspart empty - The text shown when there are no rows.
32
40
  * @csspart head - The row that holds the column headings. It has `display: contents`, so it takes no space of its own.
33
41
  * @csspart live - A live region that repeats the empty and loading text, so screen readers announce it when it changes. Visually hidden in the styled tier.
@@ -37,6 +45,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
37
45
  * @cssproperty --table-template - The column track list the table works out from its columns. The component writes it inline on the element that reads it, so setting it has no effect. Set `columns` to change the tracks.
38
46
  */
39
47
  export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
48
+ /** The word for an ascending sort, in `sorted-label`. */
49
+ ascendingLabel: string;
40
50
  /**
41
51
  * The column widths as one track list, written once on the table.
42
52
  *
@@ -68,10 +78,90 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
68
78
  * declared in it has nowhere else to be sized from.
69
79
  */
70
80
  columns: string;
81
+ /**
82
+ * The starting sort order, used until `sort` is set.
83
+ */
84
+ defaultSort: string;
85
+ /** The word for a descending sort, in `sorted-label`. */
86
+ descendingLabel: string;
71
87
  /**
72
88
  * Accessible name for the table.
89
+ *
90
+ * An `aria-label` or `aria-labelledby` on the host names the table too, and
91
+ * wins over this.
73
92
  */
74
93
  label: string;
94
+ /**
95
+ * The sort order, as `field:direction`, where `field` is a sortable column's
96
+ * `field`.
97
+ *
98
+ * The rows are compared by the text of their cells in that column, numbers by
99
+ * their value. A reader's click on a sortable heading fires `helix-sort`
100
+ * first, and refusing it leaves the order as it was. `default-sort` seeds an
101
+ * order this element then owns.
102
+ */
103
+ get sort(): string;
104
+ set sort(next: string);
105
+ /**
106
+ * What screen readers hear when the table is sorted. `{column}` is the
107
+ * column's heading and `{order}` is `ascending-label` or `descending-label`.
108
+ */
109
+ sortedLabel: string;
110
+ /** What screen readers hear when the table stops being sorted. */
111
+ unsortedLabel: string;
112
+ private sortState;
113
+ /**
114
+ * Carries a host `aria-label` and `aria-labelledby` to the grid, which is
115
+ * the element with the `table` role.
116
+ *
117
+ * A heading the table is named by is the consumer's markup, beside the
118
+ * host, and an IDREF on the host named a generic wrapper while the table
119
+ * itself kept its default name.
120
+ */
121
+ protected readonly aria: AriaForwardController;
122
+ /**
123
+ * Asks for a different order, and reports whether the request stood.
124
+ */
125
+ private requestSort;
126
+ /**
127
+ * Cycles the sort order of one column, as a click on its heading does.
128
+ *
129
+ * Ascending, descending, then the order the rows were written in.
130
+ */
131
+ toggleSort(field: string): boolean;
132
+ /**
133
+ * The rows in the order they are drawn: sorted by `sort`, and as written
134
+ * where there is no sort.
135
+ *
136
+ * **Drawn in order by assignment.** A manually assigned slot draws its nodes
137
+ * in the order they were assigned, so the rows move on screen and in the
138
+ * accessibility tree while the consumer's markup stays as it was written,
139
+ * which is what a framework holding those rows needs.
140
+ */
141
+ private get arrangedRows();
142
+ /** The sort last heard, so a render that changed nothing says nothing. */
143
+ private heardSort;
144
+ /**
145
+ * What the table says after a sort, once, in the collection's live region.
146
+ *
147
+ * A reader on a heading's button is not told that `aria-sort` moved, so the
148
+ * new order is said. Not on the first render, which is the table arriving.
149
+ */
150
+ private news;
151
+ /**
152
+ * Tells each column which way it is ordered, if it is the one.
153
+ *
154
+ * The table owns the sort and the column carries `aria-sort`, the same
155
+ * arrangement `DataTable` has.
156
+ */
157
+ private markSorted;
158
+ /**
159
+ * A click on a sortable heading's button.
160
+ *
161
+ * Enter and Space arrive here too, as the click the button makes of them.
162
+ * Only the button sorts: a click on a column's separator is a resize.
163
+ */
164
+ private readonly handleHeadClick;
75
165
  /** The columns declared as children, in the order they are written. */
76
166
  get tableColumns(): HelixHeadlessTableColumn[];
77
167
  /**
@@ -186,6 +276,7 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
186
276
  * take the outer column's edge for columns of its own that set none.
187
277
  */
188
278
  private alignCells;
279
+ protected willUpdate(changed: PropertyValues): void;
189
280
  protected updated(changed: PropertyValues): void;
190
281
  protected render(): TemplateResult;
191
282
  }
@@ -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, HelixElement, MixinCollection, splitValues } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, fillTemplate, HelixElement, HelixSortEvent, MixinCollection, requestChange, splitValues, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
+ import { cycle, format, parse, sort } from '../../data-table/root/order.js';
10
11
  import { edgeOf } from '../column/table-column.js';
11
12
  const tracksOf = (list, wanted) => {
12
13
  const words = splitValues(list, ' ');
@@ -38,12 +39,36 @@ const tracksOf = (list, wanted) => {
38
39
  const SHORT = 32;
39
40
  const FLOOR = '14ch';
40
41
  const SAMPLE = 200;
41
- const lengthOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim().length;
42
+ const textOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim();
43
+ const lengthOf = (element) => textOf(element).length;
44
+ const numberOf = (value) => Number(value.replaceAll(',', ''));
42
45
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
43
46
  constructor() {
44
47
  super(...arguments);
48
+ this.ascendingLabel = 'ascending';
45
49
  this.columns = '';
50
+ this.defaultSort = '';
51
+ this.descendingLabel = 'descending';
46
52
  this.label = '';
53
+ this.sortedLabel = 'Sorted by {column}, {order}';
54
+ this.unsortedLabel = 'Not sorted';
55
+ this.sortState = undefined;
56
+ this.aria = new AriaForwardController(this, {
57
+ label: true,
58
+ target: () => this.partNamed('table'),
59
+ });
60
+ this.heardSort = undefined;
61
+ this.handleHeadClick = (event) => {
62
+ const path = event.composedPath();
63
+ if (!path.some((node) => node instanceof Element && node.getAttribute('part') === 'sort')) {
64
+ return;
65
+ }
66
+ const columns = this.tableColumns;
67
+ const column = path.find((node) => columns.includes(node));
68
+ if (column?.canSort) {
69
+ this.toggleSort(column.field);
70
+ }
71
+ };
47
72
  this.handleSlotChange = () => {
48
73
  this.requestUpdate();
49
74
  };
@@ -51,6 +76,76 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
51
76
  this.alignment = undefined;
52
77
  this.aligned = '';
53
78
  }
79
+ get sort() {
80
+ return this.sortState ?? this.defaultSort;
81
+ }
82
+ set sort(next) {
83
+ const previous = this.sort;
84
+ this.sortState = next;
85
+ this.requestUpdate('sort', previous);
86
+ }
87
+ requestSort(next) {
88
+ return requestChange({
89
+ apply: (value) => {
90
+ const previous = this.sort;
91
+ this.sortState = value;
92
+ this.requestUpdate('sort', previous);
93
+ },
94
+ event: (value) => new HelixSortEvent({ value }),
95
+ host: this,
96
+ next,
97
+ });
98
+ }
99
+ toggleSort(field) {
100
+ return this.requestSort(format(cycle(parse(this.sort), field)));
101
+ }
102
+ get arrangedRows() {
103
+ const rows = this.slotted;
104
+ const spec = parse(this.sort);
105
+ const declared = this.tableColumns;
106
+ const index = spec
107
+ ? declared.findIndex((column) => column.canSort && column.field === spec.field)
108
+ : -1;
109
+ if (!spec || index < 0) {
110
+ return rows;
111
+ }
112
+ const records = rows.map((element) => ({ element, value: textOf(element.children[index]) }));
113
+ const numeric = records.every(({ value }) => value === '' || Number.isFinite(numberOf(value)));
114
+ const order = numeric
115
+ ? (row) => (row.value === '' ? '' : numberOf(row.value))
116
+ : (row) => row.value;
117
+ const sorted = sort(records, spec, [{ field: spec.field, order }]);
118
+ return sorted.map((record) => record.element);
119
+ }
120
+ news() {
121
+ const current = this.sort;
122
+ let said = this.announced;
123
+ if (this.hasUpdated && current !== this.heardSort) {
124
+ const parsed = parse(current);
125
+ const column = parsed
126
+ ? this.tableColumns.find((one) => one.field === parsed.field)
127
+ : undefined;
128
+ said = parsed
129
+ ? fillTemplate(this.sortedLabel, {
130
+ column: column?.textContent?.trim() || parsed.field,
131
+ order: parsed.direction === 'asc' ? this.ascendingLabel : this.descendingLabel,
132
+ })
133
+ : this.unsortedLabel;
134
+ }
135
+ this.heardSort = current;
136
+ return said;
137
+ }
138
+ markSorted() {
139
+ const current = parse(this.sort);
140
+ for (const column of this.tableColumns) {
141
+ column.sorted =
142
+ current && column.canSort && column.field === current.field
143
+ ? current.direction === 'asc'
144
+ ? 'ascending'
145
+ : 'descending'
146
+ : undefined;
147
+ }
148
+ }
54
149
  get tableColumns() {
55
150
  return this.knownChildren.filter((child) => 'isTableColumn' in child && child.isTableColumn === true);
56
151
  }
@@ -96,11 +191,13 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
96
191
  super.connectedCallback();
97
192
  this.watcher = new MutationObserver(() => this.requestUpdate());
98
193
  this.watcher.observe(this, { characterData: true, childList: true, subtree: true });
194
+ this.addEventListener('click', this.handleHeadClick);
99
195
  }
100
196
  disconnectedCallback() {
101
197
  super.disconnectedCallback();
102
198
  this.watcher?.disconnect();
103
199
  this.watcher = undefined;
200
+ this.removeEventListener('click', this.handleHeadClick);
104
201
  }
105
202
  static { this.shadowRootOptions = {
106
203
  ...HelixElement.shadowRootOptions,
@@ -123,7 +220,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
123
220
  return;
124
221
  }
125
222
  head.assign(...this.tableColumns);
126
- body.assign(...this.slotted);
223
+ body.assign(...this.arrangedRows);
127
224
  }
128
225
  alignCells() {
129
226
  if (typeof CSSStyleSheet === 'undefined' || !('replaceSync' in CSSStyleSheet.prototype)) {
@@ -146,10 +243,15 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
146
243
  }
147
244
  }
148
245
  }
246
+ willUpdate(changed) {
247
+ super.willUpdate(changed);
248
+ this.announced = this.news();
249
+ }
149
250
  updated(changed) {
150
251
  super.updated(changed);
151
252
  this.assign();
152
253
  this.alignCells();
254
+ this.markSorted();
153
255
  }
154
256
  render() {
155
257
  const template = this.template;
@@ -167,16 +269,37 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
167
269
  this.handleSlotChange();
168
270
  }}
169
271
  ></slot>
170
- ${this.renderStatus(this.hasRows)}
272
+ ${this.renderStatus(this.hasRows, 'cell')}
171
273
  </div>${this.renderLive(this.hasRows)}`;
172
274
  }
173
275
  };
276
+ __decorate([
277
+ property({ attribute: 'ascending-label' })
278
+ ], HelixHeadlessTable.prototype, "ascendingLabel", void 0);
174
279
  __decorate([
175
280
  property()
176
281
  ], HelixHeadlessTable.prototype, "columns", void 0);
282
+ __decorate([
283
+ property({ attribute: 'default-sort' })
284
+ ], HelixHeadlessTable.prototype, "defaultSort", void 0);
285
+ __decorate([
286
+ property({ attribute: 'descending-label' })
287
+ ], HelixHeadlessTable.prototype, "descendingLabel", void 0);
177
288
  __decorate([
178
289
  property()
179
290
  ], HelixHeadlessTable.prototype, "label", void 0);
291
+ __decorate([
292
+ property()
293
+ ], HelixHeadlessTable.prototype, "sort", null);
294
+ __decorate([
295
+ property({ attribute: 'sorted-label' })
296
+ ], HelixHeadlessTable.prototype, "sortedLabel", void 0);
297
+ __decorate([
298
+ property({ attribute: 'unsorted-label' })
299
+ ], HelixHeadlessTable.prototype, "unsortedLabel", void 0);
300
+ __decorate([
301
+ state()
302
+ ], HelixHeadlessTable.prototype, "sortState", void 0);
180
303
  HelixHeadlessTable = __decorate([
181
304
  customElement('helix-headless-table')
182
305
  ], HelixHeadlessTable);
@@ -32,6 +32,13 @@ export declare class HelixHeadlessTableRow extends HelixElement {
32
32
  */
33
33
  alignCells(rules: CSSStyleSheet): void;
34
34
  protected willUpdate(changed: PropertyValues): void;
35
+ /**
36
+ * Asks each cell to read its role again once the cells change.
37
+ *
38
+ * A `header` cell heads its row when a value sits beside it, and a cell that
39
+ * upgrades while its row is still being parsed has no value beside it yet.
40
+ */
41
+ private readonly handleSlotChange;
35
42
  protected render(): TemplateResult;
36
43
  }
37
44
  declare global {
@@ -11,6 +11,14 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
11
11
  super(...arguments);
12
12
  this.isTableRow = true;
13
13
  this.columnRules = undefined;
14
+ this.handleSlotChange = () => {
15
+ for (const cell of this.children) {
16
+ if ('isTableCell' in cell) {
17
+ ;
18
+ cell.requestUpdate();
19
+ }
20
+ }
21
+ };
14
22
  }
15
23
  alignCells(rules) {
16
24
  const root = this.renderRoot;
@@ -28,7 +36,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
28
36
  hostAria(this.internals, { role: 'row' });
29
37
  }
30
38
  render() {
31
- return html `<slot></slot>`;
39
+ return html `<slot @slotchange=${this.handleSlotChange}></slot>`;
32
40
  }
33
41
  };
34
42
  HelixHeadlessTableRow = __decorate([