@fusion.dev/helix-headless 0.147.0 → 0.148.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 (141) hide show
  1. package/custom-elements.json +4512 -790
  2. package/dist/accordion/item/accordion-item.d.ts +5 -2
  3. package/dist/accordion/item/accordion-item.js +11 -7
  4. package/dist/accordion/root/accordion-root.js +4 -6
  5. package/dist/announcement/root/announcement-root.d.ts +56 -5
  6. package/dist/announcement/root/announcement-root.js +124 -21
  7. package/dist/area-chart/root/area-chart-root.js +3 -4
  8. package/dist/badge/count.d.ts +14 -0
  9. package/dist/badge/count.js +20 -0
  10. package/dist/badge/root/badge-root.js +8 -22
  11. package/dist/blockquote/root/blockquote-root.d.ts +66 -8
  12. package/dist/blockquote/root/blockquote-root.js +85 -8
  13. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +2 -2
  14. package/dist/button-group/arrows.d.ts +14 -0
  15. package/dist/button-group/arrows.js +10 -0
  16. package/dist/button-group/root/button-group-root.d.ts +141 -3
  17. package/dist/button-group/root/button-group-root.js +142 -2
  18. package/dist/canvas/edge-pan.d.ts +61 -0
  19. package/dist/canvas/edge-pan.js +63 -0
  20. package/dist/canvas/node/canvas-node.d.ts +12 -2
  21. package/dist/canvas/node/canvas-node.js +6 -2
  22. package/dist/canvas/root/canvas-document.d.ts +10 -0
  23. package/dist/canvas/root/canvas-document.js +1 -0
  24. package/dist/canvas/root/canvas-root.d.ts +107 -5
  25. package/dist/canvas/root/canvas-root.js +140 -36
  26. package/dist/canvas/snap.d.ts +42 -0
  27. package/dist/canvas/snap.js +32 -0
  28. package/dist/carousel/root/carousel-root.d.ts +4 -3
  29. package/dist/carousel/root/carousel-root.js +2 -6
  30. package/dist/chat/root/chat-root.d.ts +13 -3
  31. package/dist/chat/root/chat-root.js +2 -6
  32. package/dist/checkbox-group/root/checkbox-group-root.d.ts +13 -0
  33. package/dist/checkbox-group/root/checkbox-group-root.js +18 -10
  34. package/dist/checkbox-group/select-all.d.ts +11 -0
  35. package/dist/checkbox-group/select-all.js +5 -0
  36. package/dist/code-block/root/code-block-root.d.ts +8 -6
  37. package/dist/code-block/root/code-block-root.js +4 -3
  38. package/dist/color-picker/root/color-picker-root.js +6 -12
  39. package/dist/color-picker/wheel.d.ts +11 -0
  40. package/dist/color-picker/wheel.js +9 -0
  41. package/dist/combobox/root/combobox-root.d.ts +70 -11
  42. package/dist/combobox/root/combobox-root.js +68 -18
  43. package/dist/command/root/command-root.d.ts +4 -3
  44. package/dist/data-table/root/data-table-root.js +17 -44
  45. package/dist/description-list/item/description-list-item.d.ts +4 -0
  46. package/dist/description-list/item/description-list-item.js +7 -1
  47. package/dist/divider/root/divider-root.d.ts +45 -16
  48. package/dist/divider/root/divider-root.js +28 -4
  49. package/dist/drilldown/item/drilldown-item.js +3 -4
  50. package/dist/element-events.generated.js +1 -0
  51. package/dist/empty/root/empty-root.d.ts +17 -3
  52. package/dist/empty/root/empty-root.js +29 -5
  53. package/dist/file-input/root/file-input-root.js +3 -4
  54. package/dist/file-input/status.js +2 -1
  55. package/dist/filter-input/chip/filter-chip.js +2 -3
  56. package/dist/format-date/fields.d.ts +20 -0
  57. package/dist/format-date/fields.js +14 -0
  58. package/dist/format-date/options.d.ts +2 -1
  59. package/dist/format-date/options.js +9 -38
  60. package/dist/format-date/root/format-date-root.d.ts +35 -3
  61. package/dist/format-date/root/format-date-root.js +24 -32
  62. package/dist/graph/root/graph-root.d.ts +23 -5
  63. package/dist/graph/root/graph-root.js +17 -20
  64. package/dist/heatmap/root/heatmap-root.d.ts +12 -0
  65. package/dist/heatmap/root/heatmap-root.js +26 -11
  66. package/dist/image/root/image-root.d.ts +11 -0
  67. package/dist/kbd/root/kbd-root.js +1 -2
  68. package/dist/legend/root/legend-root.js +4 -17
  69. package/dist/list/root/list-root.js +8 -22
  70. package/dist/listbox/root/listbox-root.d.ts +119 -24
  71. package/dist/listbox/root/listbox-root.js +130 -132
  72. package/dist/map/marker/map-marker.d.ts +2 -1
  73. package/dist/map/root/map-root.d.ts +40 -2
  74. package/dist/map/root/map-root.js +64 -23
  75. package/dist/megamenu/root/megamenu-root.js +4 -11
  76. package/dist/menu/root/menu-root.d.ts +2 -2
  77. package/dist/navigation/root/navigation-root.js +5 -8
  78. package/dist/notifications/group/notification-group.d.ts +8 -1
  79. package/dist/notifications/group/notification-group.js +12 -6
  80. package/dist/notifications/notification/notification.d.ts +12 -1
  81. package/dist/notifications/notification/notification.js +3 -1
  82. package/dist/notifications/root/notifications-root.d.ts +41 -5
  83. package/dist/notifications/root/notifications-root.js +27 -7
  84. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -1
  85. package/dist/object-inspector/root/object-inspector-root.js +21 -11
  86. package/dist/pagination/size/pagination-size.d.ts +1 -0
  87. package/dist/pagination/size/pagination-size.js +1 -1
  88. package/dist/pin-input/code.d.ts +23 -0
  89. package/dist/pin-input/code.js +25 -0
  90. package/dist/pin-input/root/pin-input-root.js +9 -23
  91. package/dist/qr-code/root/qr-code-root.d.ts +14 -3
  92. package/dist/qr-code/root/qr-code-root.js +19 -5
  93. package/dist/scatter-chart/measures.d.ts +22 -0
  94. package/dist/scatter-chart/measures.js +11 -0
  95. package/dist/scatter-chart/root/scatter-chart-root.d.ts +2 -6
  96. package/dist/scatter-chart/root/scatter-chart-root.js +6 -19
  97. package/dist/select/root/select-root.d.ts +56 -8
  98. package/dist/select/root/select-root.js +33 -9
  99. package/dist/speed-dial/group/speed-dial-group.d.ts +48 -0
  100. package/dist/speed-dial/group/speed-dial-group.js +43 -0
  101. package/dist/speed-dial/index.d.ts +1 -0
  102. package/dist/speed-dial/index.js +1 -0
  103. package/dist/speed-dial/item/speed-dial-item.d.ts +26 -1
  104. package/dist/speed-dial/item/speed-dial-item.js +41 -3
  105. package/dist/speed-dial/long-press.d.ts +50 -0
  106. package/dist/speed-dial/long-press.js +48 -0
  107. package/dist/speed-dial/root/speed-dial-root.d.ts +80 -1
  108. package/dist/speed-dial/root/speed-dial-root.js +114 -5
  109. package/dist/splitter/root/splitter-root.d.ts +7 -3
  110. package/dist/splitter/root/splitter-root.js +16 -3
  111. package/dist/table/cell/table-cell.d.ts +8 -1
  112. package/dist/table/cell/table-cell.js +6 -2
  113. package/dist/table/grid-controller.d.ts +117 -0
  114. package/dist/table/grid-controller.js +293 -0
  115. package/dist/table/root/table-root.d.ts +47 -3
  116. package/dist/table/root/table-root.js +81 -24
  117. package/dist/table/row/table-row.d.ts +13 -1
  118. package/dist/table/row/table-row.js +20 -6
  119. package/dist/tag/group/tag-group.js +6 -15
  120. package/dist/time-picker/root/time-picker-root.d.ts +1 -0
  121. package/dist/time-picker/root/time-picker-root.js +3 -0
  122. package/dist/toast/item/toast-item.d.ts +6 -3
  123. package/dist/toast/item/toast-item.js +1 -0
  124. package/dist/toast/root/toast-root.d.ts +13 -1
  125. package/dist/toast/root/toast-root.js +20 -14
  126. package/dist/toggle/group/toggle-group.js +3 -5
  127. package/dist/toolbar/root/toolbar-root.d.ts +31 -0
  128. package/dist/toolbar/root/toolbar-root.js +39 -8
  129. package/dist/tooltip/root/tooltip-root.js +1 -2
  130. package/dist/video/root/video-root.d.ts +11 -0
  131. package/dist/video/root/video-root.js +15 -1
  132. package/dist/virtual-list/root/virtual-list-root.js +6 -18
  133. package/lint.json +1 -1
  134. package/package.json +5 -5
  135. package/types/jsx.d.ts +2005 -266
  136. package/types/svelte.d.ts +1 -0
  137. package/types/vue.d.ts +338 -62
  138. package/dist/kbd/keys.d.ts +0 -29
  139. package/dist/kbd/keys.js +0 -52
  140. package/dist/tooltip/focus-target.d.ts +0 -6
  141. package/dist/tooltip/focus-target.js +0 -9
@@ -0,0 +1,293 @@
1
+ import { deepActiveElement, deepContains, focusableElements, gridMove, inlineStep, pageRows, scrollingAncestor, scrollRoom, selectsAll, whenHydrated, } from '@fusion.dev/helix-core';
2
+ const shown = (element) => {
3
+ const hidden = element.getAttribute('hidden');
4
+ return hidden === null || hidden === 'until-found';
5
+ };
6
+ export class TableGridController {
7
+ constructor(host, options) {
8
+ this.stopRow = undefined;
9
+ this.stopColumn = 0;
10
+ this.stopIndex = 0;
11
+ this.focusRow = undefined;
12
+ this.focusWanted = false;
13
+ this.pointing = false;
14
+ this.given = new Set();
15
+ this.handleKeydown = (event) => {
16
+ if (!this.options.on() || event.defaultPrevented || event.altKey) {
17
+ return;
18
+ }
19
+ const place = this.placeOf(event);
20
+ if (!place) {
21
+ return;
22
+ }
23
+ if (event.composedPath()[0] !== place.cell) {
24
+ this.handleControlKey(event, place);
25
+ return;
26
+ }
27
+ if (event.key === 'Tab') {
28
+ this.leave(event.shiftKey);
29
+ return;
30
+ }
31
+ if (selectsAll(event) && !event.shiftKey) {
32
+ if (this.options.selectAll()) {
33
+ event.preventDefault();
34
+ }
35
+ return;
36
+ }
37
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'F2') {
38
+ if (event.key !== 'F2' && this.options.activate(place, event)) {
39
+ event.preventDefault();
40
+ return;
41
+ }
42
+ const first = event.key === ' ' ? undefined : this.controlsIn(place)[0];
43
+ if (first) {
44
+ event.preventDefault();
45
+ first.focus();
46
+ }
47
+ return;
48
+ }
49
+ const to = gridMove(event.key, place, { last: this.options.rows().length - 1, top: this.top, width: this.widthOf(place.row) }, {
50
+ control: event.ctrlKey || event.metaKey,
51
+ page: event.key.startsWith('Page') ? this.pageFrom(place.cell) : 1,
52
+ step: inlineStep(this.host, event.key),
53
+ });
54
+ if (!to) {
55
+ return;
56
+ }
57
+ event.preventDefault();
58
+ to.column = Math.max(0, Math.min(to.column, this.widthOf(to.row) - 1));
59
+ if (to.row !== place.row || to.column !== place.column) {
60
+ this.moveTo(to);
61
+ }
62
+ };
63
+ this.handleFocusIn = (event) => {
64
+ if (!this.options.on()) {
65
+ return;
66
+ }
67
+ const place = this.placeOf(event);
68
+ if (!place) {
69
+ return;
70
+ }
71
+ const from = event.relatedTarget;
72
+ const entering = !(from instanceof Node && deepContains(this.host, from));
73
+ if (entering && !this.pointing && event.composedPath()[0] !== place.cell) {
74
+ const stop = this.cellAt(this.current());
75
+ if (stop) {
76
+ stop.focus();
77
+ return;
78
+ }
79
+ }
80
+ this.focusRow = place.row === -1 ? undefined : this.options.rows()[place.row];
81
+ const at = this.current();
82
+ if (at.row !== place.row || at.column !== place.column) {
83
+ this.moveTo(place, false);
84
+ }
85
+ };
86
+ this.handleFocusOut = (event) => {
87
+ const to = event.relatedTarget;
88
+ if (to instanceof Node && !deepContains(this.host, to)) {
89
+ this.focusRow = undefined;
90
+ }
91
+ };
92
+ this.handlePointerDown = () => {
93
+ this.pointing = true;
94
+ setTimeout(() => {
95
+ this.pointing = false;
96
+ });
97
+ };
98
+ this.host = host;
99
+ this.options = options;
100
+ host.addController(this);
101
+ }
102
+ hostConnected() {
103
+ this.host.addEventListener('keydown', this.handleKeydown);
104
+ this.host.addEventListener('focusin', this.handleFocusIn);
105
+ this.host.addEventListener('focusout', this.handleFocusOut);
106
+ this.host.addEventListener('pointerdown', this.handlePointerDown);
107
+ }
108
+ hostDisconnected() {
109
+ this.host.removeEventListener('keydown', this.handleKeydown);
110
+ this.host.removeEventListener('focusin', this.handleFocusIn);
111
+ this.host.removeEventListener('focusout', this.handleFocusOut);
112
+ this.host.removeEventListener('pointerdown', this.handlePointerDown);
113
+ }
114
+ hostUpdate() {
115
+ const gone = this.stopRow;
116
+ const rows = this.options.rows();
117
+ if (!(gone instanceof Element) || rows.includes(gone)) {
118
+ return;
119
+ }
120
+ this.stopRow = rows[Math.min(this.stopIndex, rows.length - 1)];
121
+ const active = deepActiveElement();
122
+ if (this.focusRow === gone && (!active || active === document.body)) {
123
+ this.focusWanted = true;
124
+ }
125
+ this.focusRow = undefined;
126
+ }
127
+ get offset() {
128
+ return this.options.selectable() ? 1 : 0;
129
+ }
130
+ get top() {
131
+ return this.options.columns().length > 0 || this.options.selectable() ? -1 : 0;
132
+ }
133
+ cellsOf(row) {
134
+ return [...(row?.children ?? [])].filter(shown);
135
+ }
136
+ widthOf(row) {
137
+ const own = row === -1 ? this.options.columns().length : this.cellsOf(this.options.rows()[row]).length;
138
+ return this.offset + own;
139
+ }
140
+ cellAt({ column, row }) {
141
+ const lead = column < this.offset;
142
+ if (row === -1) {
143
+ return lead
144
+ ? this.options.part('select-all')
145
+ : (this.options.columns()[column - this.offset]?.cellBox ?? null);
146
+ }
147
+ const line = this.options.rows()[row];
148
+ if (lead) {
149
+ return line?.shadowRoot?.querySelector('[part~="select"]') ?? null;
150
+ }
151
+ return this.cellsOf(line)[column - this.offset] ?? null;
152
+ }
153
+ current() {
154
+ const found = this.stopRow === undefined || this.stopRow === 'head'
155
+ ? -1
156
+ : this.options.rows().indexOf(this.stopRow);
157
+ const row = this.stopRow === 'head' || found < 0 ? this.top : found;
158
+ const width = this.widthOf(row);
159
+ return { column: Math.max(0, Math.min(this.stopColumn, width - 1)), row };
160
+ }
161
+ isStop(place) {
162
+ const at = this.current();
163
+ return this.options.on() && at.row === place.row && at.column === place.column;
164
+ }
165
+ placeOf(event) {
166
+ const columns = this.options.columns();
167
+ const rows = this.options.rows();
168
+ for (const node of event.composedPath()) {
169
+ if (node === this.host) {
170
+ return undefined;
171
+ }
172
+ if (!(node instanceof HTMLElement)) {
173
+ continue;
174
+ }
175
+ const heading = columns.indexOf(node);
176
+ if (heading >= 0) {
177
+ const cell = node.cellBox;
178
+ return cell ? { cell, column: heading + this.offset, row: -1 } : undefined;
179
+ }
180
+ const part = node.getAttribute('part') ?? '';
181
+ if (part === 'select-all' && node.getRootNode() === this.host.shadowRoot) {
182
+ return { cell: node, column: 0, row: -1 };
183
+ }
184
+ const root = node.getRootNode();
185
+ if (part.split(' ').includes('select') && rows.includes(root.host)) {
186
+ return { cell: node, column: 0, row: rows.indexOf(root.host) };
187
+ }
188
+ const parent = node.parentElement;
189
+ if (parent && rows.includes(parent) && shown(node)) {
190
+ const column = this.cellsOf(parent).indexOf(node);
191
+ return { cell: node, column: column + this.offset, row: rows.indexOf(parent) };
192
+ }
193
+ }
194
+ return undefined;
195
+ }
196
+ moveTo(place, focus = true) {
197
+ this.stopRow = place.row === -1 ? 'head' : this.options.rows()[place.row];
198
+ this.stopColumn = place.column;
199
+ this.focusWanted = focus;
200
+ this.host.requestUpdate();
201
+ }
202
+ mark() {
203
+ const on = this.options.on();
204
+ const at = this.current();
205
+ this.stopIndex = Math.max(0, at.row);
206
+ const columns = this.options.columns();
207
+ for (const [index, column] of columns.entries()) {
208
+ column.gridCell = on;
209
+ column.tabStop = on && at.row === -1 && at.column === index + this.offset;
210
+ }
211
+ for (const [index, line] of this.options.rows().entries()) {
212
+ const row = line;
213
+ if (row.isTableRow) {
214
+ whenHydrated(row, () => {
215
+ row.gridCell = on;
216
+ row.selectStop = on && at.row === index && at.column === 0 && this.offset === 1;
217
+ });
218
+ }
219
+ for (const [place, cell] of this.cellsOf(line).entries()) {
220
+ if ('isTableCell' in cell) {
221
+ ;
222
+ cell.gridCell = on;
223
+ }
224
+ if (!on) {
225
+ continue;
226
+ }
227
+ const stop = at.row === index && at.column === place + this.offset ? 0 : -1;
228
+ if (cell.tabIndex !== stop || !cell.hasAttribute('tabindex')) {
229
+ cell.tabIndex = stop;
230
+ }
231
+ this.given.add(cell);
232
+ }
233
+ }
234
+ for (const cell of this.given) {
235
+ if (!on || !cell.isConnected) {
236
+ if (!on) {
237
+ cell.removeAttribute('tabindex');
238
+ }
239
+ this.given.delete(cell);
240
+ }
241
+ }
242
+ }
243
+ place() {
244
+ if (!this.focusWanted || !this.options.on()) {
245
+ return;
246
+ }
247
+ this.focusWanted = false;
248
+ this.cellAt(this.current())?.focus();
249
+ }
250
+ controlsIn(place) {
251
+ if (place.row === -1) {
252
+ return place.column < this.offset
253
+ ? []
254
+ : (this.options.columns()[place.column - this.offset]?.controls ?? []);
255
+ }
256
+ return place.column < this.offset ? [] : focusableElements(place.cell);
257
+ }
258
+ leave(back) {
259
+ const edge = this.options.part(back ? 'grid-start' : 'grid-end');
260
+ if (!edge) {
261
+ return;
262
+ }
263
+ edge.tabIndex = 0;
264
+ edge.addEventListener('focusout', () => {
265
+ edge.tabIndex = -1;
266
+ }, { once: true });
267
+ edge.focus({ preventScroll: true });
268
+ }
269
+ pageFrom(cell) {
270
+ const room = scrollRoom(scrollingAncestor(cell, { axis: 'y', overflowing: true }));
271
+ return pageRows(room, cell.getBoundingClientRect().height);
272
+ }
273
+ handleControlKey(event, place) {
274
+ if (event.key === 'Escape' || event.key === 'F2') {
275
+ event.preventDefault();
276
+ place.cell.focus();
277
+ return;
278
+ }
279
+ if (event.key !== 'Tab') {
280
+ return;
281
+ }
282
+ const origin = event.composedPath()[0];
283
+ const controls = this.controlsIn(place);
284
+ const at = controls.findIndex((one) => deepContains(one, origin));
285
+ const next = at >= 0 ? controls[at + (event.shiftKey ? -1 : 1)] : undefined;
286
+ if (next) {
287
+ event.preventDefault();
288
+ next.focus();
289
+ return;
290
+ }
291
+ this.leave(event.shiftKey);
292
+ }
293
+ }
@@ -50,9 +50,28 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
50
50
  * 4. A chosen row takes `:state(selected)`, and a screen reader hears
51
51
  * `selected-label` ("2 selected") from a polite region.
52
52
  *
53
- * Tab reaches each checkbox, and Space ticks it. For a grid a reader moves
54
- * through with the arrow keys, with typeahead and range selection, use
55
- * `DataTable`. To act on one row, put a link or a button in a cell.
53
+ * Tab reaches each checkbox, and Space ticks it. To act on one row, put a
54
+ * link or a button in a cell, or give the row an `href`.
55
+ *
56
+ * **A grid.** `grid` makes the table one tab stop that the arrow keys move
57
+ * through cell by cell, the headings included, as React Aria's table is. The
58
+ * table is then a `grid` and each value a `gridcell`, and the table gives
59
+ * every cell a `tabindex` so focus can rest on it: set `grid` only where the
60
+ * cells may carry one. For typeahead and range selection, use `DataTable`.
61
+ *
62
+ * | Key | What it does |
63
+ * |---|---|
64
+ * | Right, Left | Moves one cell along the row. Left moves on, on a right-to-left page. |
65
+ * | Down, Up | Moves one row, the headings included. |
66
+ * | Home, End | Moves to the first or the last cell of the row. |
67
+ * | Control with Home or End | Moves to the first or the last cell of the table. |
68
+ * | Page Down, Page Up | Moves by the rows that fit in view. |
69
+ * | Enter, F2 | Moves into the cell's links and controls. Escape or F2 moves back to the cell, and Tab moves between the controls of one cell. |
70
+ * | Enter on a cell of a row with `href` | Follows the row's link. |
71
+ * | Enter, Space on a sortable heading | Sorts by the column. |
72
+ * | Space on a row, Enter or Space on its checkbox cell | Selects the row, in a `selectable` table. |
73
+ * | Control or Command with A | Selects every row, in a `selectable` table. |
74
+ * | Tab | Leaves the table. |
56
75
  *
57
76
  * The CSS custom properties listed for this component are written by it and
58
77
  * read by the stylesheet that draws it. Do not set them. Each holds a number
@@ -85,6 +104,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
85
104
  * @csspart checkbox-mark-dash - Added to `checkbox-mark` while it draws the dash, which it keeps until its box is checked.
86
105
  * @csspart checkbox-mark-indeterminate - Added to `checkbox-mark` while only some rows are selected.
87
106
  * @csspart empty - The text shown when there are no rows.
107
+ * @csspart grid-end - An empty box after the rows, in a `grid` table. Tab focuses it on its way out of the table, so the key leaves from the end.
108
+ * @csspart grid-start - An empty box before the rows, in a `grid` table. Shift with Tab focuses it on its way out of the table.
88
109
  * @csspart head - The row that holds the column headings. It has `display: contents`, so it takes no space of its own.
89
110
  * @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.
90
111
  * @csspart loading - The text shown while the rows are loading.
@@ -138,6 +159,16 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
138
159
  defaultSort: string;
139
160
  /** The word for a descending sort, in `sorted-label`. */
140
161
  descendingLabel: string;
162
+ /**
163
+ * Makes the table a grid: one tab stop, and the arrow keys, Home, End and the
164
+ * Page keys move focus between its cells, the headings included. Enter moves
165
+ * into a cell's controls, and Tab leaves the table.
166
+ *
167
+ * Off by default, where the table is a static `table` and Tab reaches each
168
+ * link and control in it. On, the table writes a `tabindex` onto each of its
169
+ * cells, which is the one way a cell of your markup can take focus.
170
+ */
171
+ grid: boolean;
141
172
  /**
142
173
  * Accessible name for the table.
143
174
  *
@@ -202,6 +233,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
202
233
  * itself kept its default name.
203
234
  */
204
235
  protected readonly aria: AriaForwardController;
236
+ /** The keyboard of a `grid` table, in `grid-controller.ts`. */
237
+ private readonly gridKeys;
205
238
  /**
206
239
  * Asks for a different order, and reports whether the request stood.
207
240
  */
@@ -248,6 +281,17 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
248
281
  * left alone.
249
282
  */
250
283
  private readonly handleToggle;
284
+ /** Every row's key, in the order the rows are written. */
285
+ private get everyKey();
286
+ /** The checkbox in the heading: every row, or none where every row is chosen. */
287
+ private chooseAll;
288
+ /** One row's checkbox, ticked or not. */
289
+ private chooseRow;
290
+ /**
291
+ * Enter, Space or F2 on a cell of a `grid` table, and whether it did
292
+ * anything. Left alone, Enter and F2 move into the cell's controls.
293
+ */
294
+ private activate;
251
295
  /**
252
296
  * Tells each row whether it is selected and what its checkbox is called.
253
297
  *
@@ -4,12 +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 { AriaForwardController, commaList, customElement, cycle, everyState, fillTemplate, format, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, assignSlots, commaList, customElement, cycle, everyState, fillTemplate, followLink, format, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import '../../checkbox/index.js';
12
12
  import { edgeOf } from '../column/table-column.js';
13
+ import { TableGridController } from '../grid-controller.js';
13
14
  const tracksOf = (list, wanted) => {
14
15
  const words = splitValues(list, ' ');
15
16
  const valid = (track) => typeof CSS === 'undefined' || CSS.supports('grid-template-columns', track);
@@ -51,6 +52,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
51
52
  this.defaultSelected = '';
52
53
  this.defaultSort = '';
53
54
  this.descendingLabel = 'descending';
55
+ this.grid = false;
54
56
  this.label = '';
55
57
  this.selectable = false;
56
58
  this.selectAllLabel = 'Select every row';
@@ -65,6 +67,15 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
65
67
  label: true,
66
68
  target: () => this.partNamed('table'),
67
69
  });
70
+ this.gridKeys = new TableGridController(this, {
71
+ activate: (place, event) => this.activate(place, event),
72
+ columns: () => this.tableColumns.filter((column) => column.shown),
73
+ on: () => this.grid,
74
+ part: (name) => this.partNamed(name),
75
+ rows: () => (this.childrenKnown ? this.arrangedRows : []),
76
+ selectAll: () => this.selectable && this.requestSelected(new Set(this.everyKey)),
77
+ selectable: () => this.selectable,
78
+ });
68
79
  this.heardSort = undefined;
69
80
  this.heardSortBefore = undefined;
70
81
  this.heardSelected = undefined;
@@ -74,10 +85,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
74
85
  return;
75
86
  }
76
87
  const root = origin.getRootNode();
77
- const keys = this.chosen;
78
88
  if (root === this.renderRoot) {
79
89
  event.preventDefault();
80
- this.requestSelected(new Set(toggleEvery(keys, this.keyed.map(({ key }) => key))));
90
+ this.chooseAll();
81
91
  return;
82
92
  }
83
93
  const found = this.keyed.find(({ row }) => row.shadowRoot === root);
@@ -85,13 +95,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
85
95
  return;
86
96
  }
87
97
  event.preventDefault();
88
- if (event.detail.checked) {
89
- keys.add(found.key);
90
- }
91
- else {
92
- keys.delete(found.key);
93
- }
94
- this.requestSelected(keys);
98
+ this.chooseRow(found.key, event.detail.checked);
95
99
  };
96
100
  this.handleHeadClick = (event) => {
97
101
  const path = event.composedPath();
@@ -207,6 +211,52 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
207
211
  next: inRowOrder(keys, this.keyed.map(({ key }) => key)).join(','),
208
212
  });
209
213
  }
214
+ get everyKey() {
215
+ return this.keyed.map(({ key }) => key);
216
+ }
217
+ chooseAll() {
218
+ this.requestSelected(new Set(toggleEvery(this.chosen, this.everyKey)));
219
+ }
220
+ chooseRow(key, on) {
221
+ const keys = this.chosen;
222
+ if (on) {
223
+ keys.add(key);
224
+ }
225
+ else {
226
+ keys.delete(key);
227
+ }
228
+ this.requestSelected(keys);
229
+ }
230
+ activate(place, event) {
231
+ const lead = this.selectable && place.column === 0;
232
+ if (event.key === 'F2') {
233
+ return false;
234
+ }
235
+ if (place.row === -1) {
236
+ if (lead) {
237
+ this.chooseAll();
238
+ return true;
239
+ }
240
+ const shown = this.tableColumns.filter((one) => one.shown);
241
+ const column = shown[place.column - (this.selectable ? 1 : 0)];
242
+ if (column?.canSort) {
243
+ this.toggleSort(column.field);
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+ const row = this.arrangedRows[place.row];
249
+ const found = this.keyed.find((one) => one.row === row);
250
+ if (this.selectable && found && (event.key === ' ' || lead)) {
251
+ this.chooseRow(found.key, !this.chosen.has(found.key));
252
+ return true;
253
+ }
254
+ if (event.key === 'Enter' && row?.href) {
255
+ followLink(this.ownerDocument, { href: row.href, rel: row.rel, target: row.target }, event);
256
+ return true;
257
+ }
258
+ return false;
259
+ }
210
260
  markSelected() {
211
261
  const keys = this.chosen;
212
262
  for (const { key, row } of this.keyed) {
@@ -325,16 +375,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
325
375
  }
326
376
  return;
327
377
  }
328
- const head = this.renderRoot?.querySelector('slot[name="columns"]');
329
- const body = this.renderRoot?.querySelector('slot:not([name])');
330
- if (!head || !body) {
331
- return;
332
- }
333
- head.assign(...this.tableColumns);
334
- body.assign(...this.arrangedRows);
335
- this.renderRoot
336
- ?.querySelector('slot[name="heading"]')
337
- ?.assign(...this.headings);
378
+ const columns = new Set(this.tableColumns);
379
+ const headings = new Set(this.headings);
380
+ assignSlots(this.renderRoot, [...columns, ...headings, ...this.arrangedRows], (child) => (columns.has(child) ? 'columns' : headings.has(child) ? 'heading' : ''));
338
381
  }
339
382
  alignCells() {
340
383
  if (typeof CSSStyleSheet === 'undefined' || !('replaceSync' in CSSStyleSheet.prototype)) {
@@ -368,6 +411,10 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
368
411
  this.markSorted();
369
412
  this.markSelected();
370
413
  this.nameByHeading();
414
+ if (this.childrenKnown) {
415
+ this.gridKeys.mark();
416
+ this.gridKeys.place();
417
+ }
371
418
  }
372
419
  renderSelectAll() {
373
420
  if (!this.selectable) {
@@ -376,9 +423,12 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
376
423
  const every = this.childrenKnown ? this.keyed.map(({ key }) => key) : [];
377
424
  const { checked, indeterminate } = everyState(this.chosen, every);
378
425
  const checkbox = this.tag('checkbox');
426
+ const grid = this.grid;
379
427
  return html `<span part="select-all" role="columnheader"
428
+ tabindex=${grid ? (this.gridKeys.isStop({ column: 0, row: -1 }) ? '0' : '-1') : nothing}
380
429
  ><${checkbox}
381
430
  part="checkbox"
431
+ tabindex=${grid ? '-1' : nothing}
382
432
  exportparts="box: checkbox-box, control: checkbox-control, feedback: checkbox-feedback,
383
433
  field: checkbox-field, hint: checkbox-hint, label: checkbox-label, mark: checkbox-mark,
384
434
  mark-checked: checkbox-mark-checked, mark-dash: checkbox-mark-dash,
@@ -391,9 +441,13 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
391
441
  }
392
442
  render() {
393
443
  const template = this.template;
394
- return html `<slot name="heading"></slot><div
444
+ const grid = this.grid;
445
+ const start = grid ? html `<span part="grid-start" tabindex="-1"></span>` : nothing;
446
+ const end = grid ? html `<span part="grid-end" tabindex="-1"></span>` : nothing;
447
+ return html `<slot name="heading"></slot>${start}<div
395
448
  part="table"
396
- role="table"
449
+ role=${grid ? 'grid' : 'table'}
450
+ aria-multiselectable=${grid && this.selectable ? 'true' : nothing}
397
451
  aria-busy=${this.loading ? 'true' : nothing}
398
452
  aria-label=${this.label || 'Table'}
399
453
  style=${template ? `--table-template: ${template}` : ''}
@@ -405,8 +459,8 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
405
459
  this.handleSlotChange();
406
460
  }}
407
461
  ></slot>
408
- ${this.renderStatus(this.hasRows, 'cell')}
409
- </div>${this.renderLive(this.hasRows)}`;
462
+ ${this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')}
463
+ </div>${end}${this.renderLive(this.hasRows)}`;
410
464
  }
411
465
  };
412
466
  __decorate([
@@ -424,6 +478,9 @@ __decorate([
424
478
  __decorate([
425
479
  property({ attribute: 'descending-label' })
426
480
  ], HelixHeadlessTable.prototype, "descendingLabel", void 0);
481
+ __decorate([
482
+ property({ type: Boolean })
483
+ ], HelixHeadlessTable.prototype, "grid", void 0);
427
484
  __decorate([
428
485
  property()
429
486
  ], HelixHeadlessTable.prototype, "label", void 0);
@@ -18,7 +18,8 @@ import '../../checkbox/index.js';
18
18
  * of the row's own, and Control, Command or Shift open it the way they open a
19
19
  * link. A drag that selects text follows nothing. A row in a table is not a
20
20
  * stop of its own, so put the same link in a cell, usually the row header,
21
- * which is what the keyboard and a screen reader reach.
21
+ * which is what the keyboard and a screen reader reach. In a `grid` table,
22
+ * Enter on any cell of the row follows it as well.
22
23
  *
23
24
  * @since 0.1.0
24
25
  * @status stable
@@ -76,6 +77,17 @@ export declare class HelixHeadlessTableRow extends HelixElement {
76
77
  selected: boolean;
77
78
  /** @internal The name of the row's checkbox, written by the table. Not a consumer's to set. */
78
79
  selectLabel: string;
80
+ /**
81
+ * @internal Whether the row is a row of a `grid` table, written by that
82
+ * table. Not a consumer's to set. The checkbox cell is then a `gridcell`
83
+ * that takes focus, and the row says whether it is selected.
84
+ */
85
+ gridCell: boolean;
86
+ /**
87
+ * @internal Whether the checkbox cell holds the grid's one tab stop, written
88
+ * by the table that sets `gridCell`. Not a consumer's to set.
89
+ */
90
+ selectStop: boolean;
79
91
  /** The table's rules this row holds, so the next ones replace them rather than pile up. */
80
92
  private columnRules;
81
93
  /**
@@ -4,12 +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, followLink, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, followLink, HelixElement, hostAria, pressedOwnControl, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import '../../checkbox/index.js';
12
- const PRESSABLE = 'a[href], button, input, label, select, summary, textarea, [tabindex]';
13
12
  let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
14
13
  constructor() {
15
14
  super(...arguments);
@@ -21,14 +20,17 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
21
20
  this.selectable = false;
22
21
  this.selected = false;
23
22
  this.selectLabel = '';
23
+ this.gridCell = false;
24
+ this.selectStop = false;
24
25
  this.columnRules = undefined;
25
26
  this.handleClick = (event) => {
26
27
  if (this.href === '' || event.button !== 0 || event.defaultPrevented) {
27
28
  return;
28
29
  }
29
- const path = event.composedPath();
30
- const inner = path.slice(0, Math.max(0, path.indexOf(this)));
31
- if (inner.some((node) => node instanceof Element && node.matches(PRESSABLE))) {
30
+ const cell = event
31
+ .composedPath()
32
+ .find((node) => node instanceof Element && node.parentElement === this && 'isTableCell' in node);
33
+ if (pressedOwnControl(event, cell ?? this)) {
32
34
  return;
33
35
  }
34
36
  if (globalThis.getSelection?.()?.toString()) {
@@ -59,6 +61,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
59
61
  willUpdate(changed) {
60
62
  super.willUpdate(changed);
61
63
  hostAria(this.internals, { role: 'row' });
64
+ this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
62
65
  this.toggleState('selected', this.selectable && this.selected);
63
66
  this.toggleState('link', this.href !== '');
64
67
  }
@@ -75,8 +78,13 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
75
78
  return nothing;
76
79
  }
77
80
  const checkbox = this.tag('checkbox');
78
- return html `<span part="select${this.selected ? ' select-selected' : ''}" role="cell"
81
+ const grid = this.gridCell;
82
+ return html `<span
83
+ part="select${this.selected ? ' select-selected' : ''}"
84
+ role=${grid ? 'gridcell' : 'cell'}
85
+ tabindex=${grid ? (this.selectStop ? '0' : '-1') : nothing}
79
86
  ><${checkbox}
87
+ tabindex=${grid ? '-1' : nothing}
80
88
  part="checkbox"
81
89
  exportparts="box: checkbox-box, control: checkbox-control, feedback: checkbox-feedback,
82
90
  field: checkbox-field, hint: checkbox-hint, label: checkbox-label, mark: checkbox-mark,
@@ -112,6 +120,12 @@ __decorate([
112
120
  __decorate([
113
121
  state()
114
122
  ], HelixHeadlessTableRow.prototype, "selectLabel", void 0);
123
+ __decorate([
124
+ state()
125
+ ], HelixHeadlessTableRow.prototype, "gridCell", void 0);
126
+ __decorate([
127
+ state()
128
+ ], HelixHeadlessTableRow.prototype, "selectStop", void 0);
115
129
  HelixHeadlessTableRow = __decorate([
116
130
  customElement('helix-headless-table-row')
117
131
  ], HelixHeadlessTableRow);