@navikt/ds-react 8.4.1 → 8.5.1

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 (110) hide show
  1. package/cjs/accordion/Accordion.d.ts +10 -0
  2. package/cjs/accordion/Accordion.js +2 -2
  3. package/cjs/accordion/Accordion.js.map +1 -1
  4. package/cjs/data/table/helpers/table-cell.d.ts +2 -2
  5. package/cjs/data/table/helpers/table-cell.js +2 -5
  6. package/cjs/data/table/helpers/table-cell.js.map +1 -1
  7. package/cjs/data/table/helpers/table-focus.d.ts +26 -2
  8. package/cjs/data/table/helpers/table-focus.js +60 -9
  9. package/cjs/data/table/helpers/table-focus.js.map +1 -1
  10. package/cjs/data/table/helpers/table-grid-nav.d.ts +40 -10
  11. package/cjs/data/table/helpers/table-grid-nav.js +102 -25
  12. package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
  13. package/cjs/data/table/helpers/table-keyboard.d.ts +24 -3
  14. package/cjs/data/table/helpers/table-keyboard.js +25 -5
  15. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  16. package/cjs/data/table/hooks/useGridCache.d.ts +17 -0
  17. package/cjs/data/table/hooks/useGridCache.js +65 -0
  18. package/cjs/data/table/hooks/useGridCache.js.map +1 -0
  19. package/cjs/data/table/root/DataTableRoot.d.ts +14 -4
  20. package/cjs/data/table/root/DataTableRoot.js +4 -6
  21. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  22. package/cjs/data/table/root/useTableKeyboardNav.d.ts +10 -4
  23. package/cjs/data/table/root/useTableKeyboardNav.js +70 -99
  24. package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
  25. package/cjs/data/token-filter/AutoSuggest.d.ts +21 -0
  26. package/cjs/data/token-filter/AutoSuggest.js +129 -0
  27. package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
  28. package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
  29. package/cjs/data/token-filter/TokenFilter.js +91 -0
  30. package/cjs/data/token-filter/TokenFilter.js.map +1 -0
  31. package/cjs/data/token-filter/TokenFilter.types.d.ts +46 -0
  32. package/cjs/data/token-filter/TokenFilter.types.js +3 -0
  33. package/cjs/data/token-filter/TokenFilter.types.js.map +1 -0
  34. package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  35. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +171 -0
  36. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  37. package/cjs/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  38. package/cjs/data/token-filter/helpers/parse-query-text.js +91 -0
  39. package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
  40. package/cjs/link-card/LinkCard.d.ts +13 -0
  41. package/cjs/link-card/LinkCard.js +2 -2
  42. package/cjs/link-card/LinkCard.js.map +1 -1
  43. package/cjs/process/Process.d.ts +1 -1
  44. package/cjs/tooltip/Tooltip.js +1 -1
  45. package/cjs/tooltip/Tooltip.js.map +1 -1
  46. package/esm/accordion/Accordion.d.ts +10 -0
  47. package/esm/accordion/Accordion.js +2 -2
  48. package/esm/accordion/Accordion.js.map +1 -1
  49. package/esm/data/table/helpers/table-cell.d.ts +2 -2
  50. package/esm/data/table/helpers/table-cell.js +2 -5
  51. package/esm/data/table/helpers/table-cell.js.map +1 -1
  52. package/esm/data/table/helpers/table-focus.d.ts +26 -2
  53. package/esm/data/table/helpers/table-focus.js +55 -9
  54. package/esm/data/table/helpers/table-focus.js.map +1 -1
  55. package/esm/data/table/helpers/table-grid-nav.d.ts +40 -10
  56. package/esm/data/table/helpers/table-grid-nav.js +96 -24
  57. package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
  58. package/esm/data/table/helpers/table-keyboard.d.ts +24 -3
  59. package/esm/data/table/helpers/table-keyboard.js +24 -4
  60. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  61. package/esm/data/table/hooks/useGridCache.d.ts +17 -0
  62. package/esm/data/table/hooks/useGridCache.js +63 -0
  63. package/esm/data/table/hooks/useGridCache.js.map +1 -0
  64. package/esm/data/table/root/DataTableRoot.d.ts +14 -4
  65. package/esm/data/table/root/DataTableRoot.js +4 -6
  66. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  67. package/esm/data/table/root/useTableKeyboardNav.d.ts +10 -4
  68. package/esm/data/table/root/useTableKeyboardNav.js +75 -104
  69. package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
  70. package/esm/data/token-filter/AutoSuggest.d.ts +21 -0
  71. package/esm/data/token-filter/AutoSuggest.js +93 -0
  72. package/esm/data/token-filter/AutoSuggest.js.map +1 -0
  73. package/esm/data/token-filter/TokenFilter.d.ts +11 -0
  74. package/esm/data/token-filter/TokenFilter.js +55 -0
  75. package/esm/data/token-filter/TokenFilter.js.map +1 -0
  76. package/esm/data/token-filter/TokenFilter.types.d.ts +46 -0
  77. package/esm/data/token-filter/TokenFilter.types.js +2 -0
  78. package/esm/data/token-filter/TokenFilter.types.js.map +1 -0
  79. package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  80. package/esm/data/token-filter/helpers/generate-autocomplete-options.js +169 -0
  81. package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  82. package/esm/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  83. package/esm/data/token-filter/helpers/parse-query-text.js +87 -0
  84. package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
  85. package/esm/link-card/LinkCard.d.ts +13 -0
  86. package/esm/link-card/LinkCard.js +2 -2
  87. package/esm/link-card/LinkCard.js.map +1 -1
  88. package/esm/process/Process.d.ts +1 -1
  89. package/esm/tooltip/Tooltip.js +2 -2
  90. package/esm/tooltip/Tooltip.js.map +1 -1
  91. package/package.json +3 -3
  92. package/src/accordion/Accordion.tsx +19 -2
  93. package/src/data/table/helpers/table-cell.ts +2 -7
  94. package/src/data/table/helpers/table-focus.ts +70 -9
  95. package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
  96. package/src/data/table/helpers/table-grid-nav.ts +128 -32
  97. package/src/data/table/helpers/table-keyboard.test.ts +27 -27
  98. package/src/data/table/helpers/table-keyboard.ts +34 -4
  99. package/src/data/table/hooks/useGridCache.ts +73 -0
  100. package/src/data/table/root/DataTableRoot.tsx +21 -11
  101. package/src/data/table/root/useTableKeyboardNav.ts +110 -128
  102. package/src/data/token-filter/AutoSuggest.tsx +179 -0
  103. package/src/data/token-filter/TokenFilter.tsx +124 -0
  104. package/src/data/token-filter/TokenFilter.types.ts +79 -0
  105. package/src/data/token-filter/helpers/generate-autocomplete-options.ts +244 -0
  106. package/src/data/token-filter/helpers/parse-query-text.test.ts +410 -0
  107. package/src/data/token-filter/helpers/parse-query-text.ts +148 -0
  108. package/src/link-card/LinkCard.tsx +15 -1
  109. package/src/process/Process.tsx +1 -1
  110. package/src/tooltip/Tooltip.tsx +3 -3
@@ -0,0 +1,659 @@
1
+ import { afterEach, describe, expect, test } from "vitest";
2
+ import {
3
+ buildTableGridMap,
4
+ findFirstCell,
5
+ findFirstCellInRow,
6
+ findLastCell,
7
+ findLastCellInRow,
8
+ findNextFocusableCell,
9
+ getNextGridPosition,
10
+ isCellFocusable,
11
+ } from "./table-grid-nav";
12
+
13
+ let container: HTMLDivElement;
14
+
15
+ afterEach(() => {
16
+ container?.parentNode && document.body.removeChild(container);
17
+ });
18
+
19
+ function createTable(html: string): HTMLTableElement {
20
+ container = document.createElement("div");
21
+ container.innerHTML = html;
22
+ document.body.appendChild(container);
23
+ return container.querySelector("table")!;
24
+ }
25
+
26
+ describe("buildTableGridMap", () => {
27
+ test("should build grid for simple 2x2 table without spans", () => {
28
+ const table = createTable(`
29
+ <table>
30
+ <tr>
31
+ <td>A1</td>
32
+ <td>B1</td>
33
+ </tr>
34
+ <tr>
35
+ <td>A2</td>
36
+ <td>B2</td>
37
+ </tr>
38
+ </table>
39
+ `);
40
+
41
+ const { grid, positions } = buildTableGridMap(table);
42
+
43
+ expect(grid.length).toBe(2);
44
+ expect(grid[0].length).toBe(2);
45
+ expect(grid[1].length).toBe(2);
46
+
47
+ const cells = Array.from(table.querySelectorAll("td"));
48
+ expect(grid[0][0]).toBe(cells[0]);
49
+ expect(grid[0][1]).toBe(cells[1]);
50
+ expect(grid[1][0]).toBe(cells[2]);
51
+ expect(grid[1][1]).toBe(cells[3]);
52
+
53
+ expect(positions.get(cells[0])).toEqual({ x: 0, y: 0 });
54
+ expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
55
+ expect(positions.get(cells[2])).toEqual({ x: 0, y: 1 });
56
+ expect(positions.get(cells[3])).toEqual({ x: 1, y: 1 });
57
+ });
58
+
59
+ test("should handle colspan correctly", () => {
60
+ const table = createTable(`
61
+ <table>
62
+ <tr>
63
+ <td colspan="2">A1-B1</td>
64
+ </tr>
65
+ <tr>
66
+ <td>A2</td>
67
+ <td>B2</td>
68
+ </tr>
69
+ </table>
70
+ `);
71
+
72
+ const { grid, positions } = buildTableGridMap(table);
73
+
74
+ expect(grid.length).toBe(2);
75
+ expect(grid[0].length).toBe(2);
76
+ expect(grid[1].length).toBe(2);
77
+
78
+ const cells = Array.from(table.querySelectorAll("td"));
79
+ const cellWithColspan = cells[0];
80
+
81
+ expect(grid[0][0]).toBe(cellWithColspan);
82
+ expect(grid[0][1]).toBe(cellWithColspan);
83
+ expect(grid[1][0]).toBe(cells[1]);
84
+ expect(grid[1][1]).toBe(cells[2]);
85
+
86
+ expect(positions.get(cellWithColspan)).toEqual({ x: 0, y: 0 });
87
+ expect(positions.get(cells[1])).toEqual({ x: 0, y: 1 });
88
+ expect(positions.get(cells[2])).toEqual({ x: 1, y: 1 });
89
+ });
90
+
91
+ test("should handle rowspan correctly", () => {
92
+ const table = createTable(`
93
+ <table>
94
+ <tr>
95
+ <td rowspan="2">A1-A2</td>
96
+ <td>B1</td>
97
+ </tr>
98
+ <tr>
99
+ <td>B2</td>
100
+ </tr>
101
+ </table>
102
+ `);
103
+
104
+ const { grid, positions } = buildTableGridMap(table);
105
+
106
+ expect(grid.length).toBe(2);
107
+ expect(grid[0].length).toBe(2);
108
+ expect(grid[1].length).toBe(2);
109
+
110
+ const cells = Array.from(table.querySelectorAll("td"));
111
+ const cellWithRowspan = cells[0];
112
+
113
+ expect(grid[0][0]).toBe(cellWithRowspan);
114
+ expect(grid[0][1]).toBe(cells[1]);
115
+ expect(grid[1][0]).toBe(cellWithRowspan);
116
+ expect(grid[1][1]).toBe(cells[2]);
117
+
118
+ expect(positions.get(cellWithRowspan)).toEqual({ x: 0, y: 0 });
119
+ expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
120
+ expect(positions.get(cells[2])).toEqual({ x: 1, y: 1 });
121
+ });
122
+
123
+ test("should handle both colspan and rowspan", () => {
124
+ const table = createTable(`
125
+ <table>
126
+ <tr>
127
+ <td colspan="2" rowspan="2">A1-B1-A2-B2</td>
128
+ <td>C1</td>
129
+ </tr>
130
+ <tr>
131
+ <td>C2</td>
132
+ </tr>
133
+ </table>
134
+ `);
135
+
136
+ const { grid, positions } = buildTableGridMap(table);
137
+
138
+ expect(grid.length).toBe(2);
139
+ expect(grid[0].length).toBe(3);
140
+ expect(grid[1].length).toBe(3);
141
+
142
+ const cells = Array.from(table.querySelectorAll("td"));
143
+ const spanningCell = cells[0];
144
+
145
+ expect(grid[0][0]).toBe(spanningCell);
146
+ expect(grid[0][1]).toBe(spanningCell);
147
+ expect(grid[0][2]).toBe(cells[1]);
148
+ expect(grid[1][0]).toBe(spanningCell);
149
+ expect(grid[1][1]).toBe(spanningCell);
150
+ expect(grid[1][2]).toBe(cells[2]);
151
+
152
+ expect(positions.get(spanningCell)).toEqual({ x: 0, y: 0 });
153
+ expect(positions.get(cells[1])).toEqual({ x: 2, y: 0 });
154
+ expect(positions.get(cells[2])).toEqual({ x: 2, y: 1 });
155
+ });
156
+
157
+ test("should handle complex table with multiple spans", () => {
158
+ const table = createTable(`
159
+ <table>
160
+ <tr>
161
+ <td>A1</td>
162
+ <td colspan="2">B1-C1</td>
163
+ <td>D1</td>
164
+ </tr>
165
+ <tr>
166
+ <td rowspan="2">A2-A3</td>
167
+ <td>B2</td>
168
+ <td>C2</td>
169
+ <td>D2</td>
170
+ </tr>
171
+ <tr>
172
+ <td>B3</td>
173
+ <td colspan="2">C3-D3</td>
174
+ </tr>
175
+ </table>
176
+ `);
177
+
178
+ const { grid, positions } = buildTableGridMap(table);
179
+
180
+ expect(grid.length).toBe(3);
181
+ expect(grid[0].length).toBe(4);
182
+ expect(grid[1].length).toBe(4);
183
+ expect(grid[2].length).toBe(4);
184
+
185
+ const cells = Array.from(table.querySelectorAll("td"));
186
+
187
+ expect(grid[0][0]).toBe(cells[0]);
188
+ expect(grid[0][1]).toBe(cells[1]);
189
+ expect(grid[0][2]).toBe(cells[1]);
190
+ expect(grid[0][3]).toBe(cells[2]);
191
+
192
+ expect(grid[1][0]).toBe(cells[3]);
193
+ expect(grid[1][1]).toBe(cells[4]);
194
+ expect(grid[1][2]).toBe(cells[5]);
195
+ expect(grid[1][3]).toBe(cells[6]);
196
+
197
+ expect(grid[2][0]).toBe(cells[3]);
198
+ expect(grid[2][1]).toBe(cells[7]);
199
+ expect(grid[2][2]).toBe(cells[8]);
200
+ expect(grid[2][3]).toBe(cells[8]);
201
+
202
+ expect(positions.get(cells[0])).toEqual({ x: 0, y: 0 });
203
+ expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
204
+ expect(positions.get(cells[2])).toEqual({ x: 3, y: 0 });
205
+ expect(positions.get(cells[3])).toEqual({ x: 0, y: 1 });
206
+ expect(positions.get(cells[4])).toEqual({ x: 1, y: 1 });
207
+ expect(positions.get(cells[5])).toEqual({ x: 2, y: 1 });
208
+ expect(positions.get(cells[6])).toEqual({ x: 3, y: 1 });
209
+ expect(positions.get(cells[7])).toEqual({ x: 1, y: 2 });
210
+ expect(positions.get(cells[8])).toEqual({ x: 2, y: 2 });
211
+ });
212
+
213
+ test("should handle table with thead, tbody, and tfoot", () => {
214
+ const table = createTable(`
215
+ <table>
216
+ <thead>
217
+ <tr>
218
+ <th>Header 1</th>
219
+ <th>Header 2</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody>
223
+ <tr>
224
+ <td>Body 1</td>
225
+ <td>Body 2</td>
226
+ </tr>
227
+ </tbody>
228
+ <tfoot>
229
+ <tr>
230
+ <td>Footer 1</td>
231
+ <td>Footer 2</td>
232
+ </tr>
233
+ </tfoot>
234
+ </table>
235
+ `);
236
+
237
+ const { grid } = buildTableGridMap(table);
238
+
239
+ expect(grid.length).toBe(3);
240
+ expect(grid[0].length).toBe(2);
241
+ expect(grid[1].length).toBe(2);
242
+ expect(grid[2].length).toBe(2);
243
+
244
+ const headerCells = Array.from(table.querySelectorAll("th"));
245
+ const bodyCells = Array.from(table.querySelectorAll("tbody td"));
246
+ const footerCells = Array.from(table.querySelectorAll("tfoot td"));
247
+
248
+ expect(grid[0][0]).toBe(headerCells[0]);
249
+ expect(grid[0][1]).toBe(headerCells[1]);
250
+ expect(grid[1][0]).toBe(bodyCells[0]);
251
+ expect(grid[1][1]).toBe(bodyCells[1]);
252
+ expect(grid[2][0]).toBe(footerCells[0]);
253
+ expect(grid[2][1]).toBe(footerCells[1]);
254
+ });
255
+
256
+ test("should handle empty table", () => {
257
+ const table = createTable("<table></table>");
258
+
259
+ const { grid, positions } = buildTableGridMap(table);
260
+
261
+ expect(grid.length).toBe(0);
262
+ expect(positions.size).toBe(0);
263
+ });
264
+
265
+ test("should handle table with empty row", () => {
266
+ const table = createTable(`
267
+ <table>
268
+ <tr></tr>
269
+ </table>
270
+ `);
271
+
272
+ const { grid, positions } = buildTableGridMap(table);
273
+
274
+ expect(grid.length).toBe(1);
275
+ expect(grid[0].length).toBe(0);
276
+ expect(positions.size).toBe(0);
277
+ });
278
+
279
+ test("should handle colspan=0 and rowspan=0 as 1", () => {
280
+ const table = createTable(`
281
+ <table>
282
+ <tr>
283
+ <td colspan="0">A</td>
284
+ <td>B</td>
285
+ </tr>
286
+ <tr>
287
+ <td rowspan="0">C</td>
288
+ <td>D</td>
289
+ </tr>
290
+ </table>
291
+ `);
292
+
293
+ const { grid } = buildTableGridMap(table);
294
+ const cells = Array.from(table.querySelectorAll("td"));
295
+
296
+ expect(grid[0][0]).toBe(cells[0]);
297
+ expect(grid[0][1]).toBe(cells[1]);
298
+ expect(grid[1][0]).toBe(cells[2]);
299
+ expect(grid[1][1]).toBe(cells[3]);
300
+ });
301
+
302
+ test("should handle negative span values as 1", () => {
303
+ const table = createTable(`
304
+ <table>
305
+ <tr>
306
+ <td colspan="-1">A</td>
307
+ <td>B</td>
308
+ </tr>
309
+ <tr>
310
+ <td rowspan="-2">C</td>
311
+ <td>D</td>
312
+ </tr>
313
+ </table>
314
+ `);
315
+
316
+ const { grid } = buildTableGridMap(table);
317
+ const cells = Array.from(table.querySelectorAll("td"));
318
+
319
+ expect(grid[0][0]).toBe(cells[0]);
320
+ expect(grid[0][1]).toBe(cells[1]);
321
+ expect(grid[1][0]).toBe(cells[2]);
322
+ expect(grid[1][1]).toBe(cells[3]);
323
+ });
324
+
325
+ test("should handle large span values", () => {
326
+ const table = createTable(`
327
+ <table>
328
+ <tr>
329
+ <td colspan="5">Wide cell</td>
330
+ </tr>
331
+ <tr>
332
+ <td>A</td>
333
+ <td>B</td>
334
+ <td>C</td>
335
+ <td>D</td>
336
+ <td>E</td>
337
+ </tr>
338
+ </table>
339
+ `);
340
+
341
+ const { grid, positions } = buildTableGridMap(table);
342
+ const cells = Array.from(table.querySelectorAll("td"));
343
+ const wideCell = cells[0];
344
+
345
+ expect(grid[0].length).toBe(5);
346
+ expect(grid[0][0]).toBe(wideCell);
347
+ expect(grid[0][1]).toBe(wideCell);
348
+ expect(grid[0][2]).toBe(wideCell);
349
+ expect(grid[0][3]).toBe(wideCell);
350
+ expect(grid[0][4]).toBe(wideCell);
351
+
352
+ expect(positions.get(wideCell)).toEqual({ x: 0, y: 0 });
353
+ });
354
+
355
+ test("should skip over slots occupied by previous spans", () => {
356
+ const table = createTable(`
357
+ <table>
358
+ <tr>
359
+ <td rowspan="2">A</td>
360
+ <td>B</td>
361
+ <td>C</td>
362
+ </tr>
363
+ <tr>
364
+ <td>D</td>
365
+ <td>E</td>
366
+ </tr>
367
+ </table>
368
+ `);
369
+
370
+ const { grid, positions } = buildTableGridMap(table);
371
+ const cells = Array.from(table.querySelectorAll("td"));
372
+
373
+ expect(grid[0][0]).toBe(cells[0]);
374
+ expect(grid[0][1]).toBe(cells[1]);
375
+ expect(grid[0][2]).toBe(cells[2]);
376
+ expect(grid[1][0]).toBe(cells[0]);
377
+ expect(grid[1][1]).toBe(cells[3]);
378
+ expect(grid[1][2]).toBe(cells[4]);
379
+
380
+ expect(positions.get(cells[3])).toEqual({ x: 1, y: 1 });
381
+ expect(positions.get(cells[4])).toEqual({ x: 2, y: 1 });
382
+ });
383
+ });
384
+
385
+ describe("getNextGridPosition", () => {
386
+ test("should return null when moving out of bounds", () => {
387
+ const grid = [
388
+ [undefined, undefined],
389
+ [undefined, undefined],
390
+ ];
391
+ const down = getNextGridPosition(grid, { x: 0, y: 1 }, { x: 0, y: 1 });
392
+ const up = getNextGridPosition(grid, { x: 0, y: 0 }, { x: 0, y: -1 });
393
+ const right = getNextGridPosition(grid, { x: 1, y: 0 }, { x: 1, y: 0 });
394
+ const left = getNextGridPosition(grid, { x: 0, y: 0 }, { x: -1, y: 0 });
395
+
396
+ expect(down).toBeNull();
397
+ expect(up).toBeNull();
398
+ expect(right).toBeNull();
399
+ expect(left).toBeNull();
400
+ });
401
+
402
+ test("should handle empty grid", () => {
403
+ const grid: (Element | undefined)[][] = [];
404
+ const result = getNextGridPosition(grid, { x: 0, y: 0 }, { x: 1, y: 0 });
405
+ expect(result).toBeNull();
406
+ });
407
+ });
408
+
409
+ describe("isCellFocusable", () => {
410
+ test("should return false when cell is undefined", () => {
411
+ expect(isCellFocusable(undefined)).toBe(false);
412
+ });
413
+
414
+ test("should return true when cell has focusable elements", () => {
415
+ const table = createTable(`
416
+ <table>
417
+ <tr>
418
+ <td>A</td>
419
+ <td><button>B</button></td>
420
+ </tr>
421
+ </table>
422
+ `);
423
+ const cells = table.querySelectorAll("td");
424
+
425
+ expect(isCellFocusable(cells[1])).toBe(true);
426
+ });
427
+ });
428
+
429
+ describe("findNextFocusableCell", () => {
430
+ test("should find next focusable cell to the right", () => {
431
+ const table = createTable(`
432
+ <table>
433
+ <tr>
434
+ <td><button>A</button></td>
435
+ <td><button>B</button></td>
436
+ <td><button>C</button></td>
437
+ </tr>
438
+ </table>
439
+ `);
440
+
441
+ const { grid, positions } = buildTableGridMap(table);
442
+ const cells = Array.from(table.querySelectorAll("td"));
443
+
444
+ const currentPos = positions.get(cells[0])!;
445
+ const result = findNextFocusableCell(
446
+ grid,
447
+ currentPos,
448
+ { x: 1, y: 0 },
449
+ cells[0],
450
+ );
451
+
452
+ expect(result).toBe(cells[1]);
453
+ });
454
+
455
+ test("should not skip non-focusable cells", () => {
456
+ const table = createTable(`
457
+ <table>
458
+ <tr>
459
+ <td><button>A</button></td>
460
+ <td>B</td>
461
+ <td><button>C</button></td>
462
+ </tr>
463
+ </table>
464
+ `);
465
+
466
+ const { grid, positions } = buildTableGridMap(table);
467
+ const cells = Array.from(table.querySelectorAll("td"));
468
+
469
+ const currentPos = positions.get(cells[0])!;
470
+ const result = findNextFocusableCell(
471
+ grid,
472
+ currentPos,
473
+ { x: 1, y: 0 },
474
+ cells[0],
475
+ );
476
+
477
+ expect(result).toBe(cells[1]);
478
+ });
479
+
480
+ test("should return null when reaching edge of grid", () => {
481
+ const table = createTable(`
482
+ <table>
483
+ <tr>
484
+ <td><button>A</button></td>
485
+ <td>B</td>
486
+ </tr>
487
+ </table>
488
+ `);
489
+
490
+ const { grid, positions } = buildTableGridMap(table);
491
+ const cells = Array.from(table.querySelectorAll("td"));
492
+
493
+ const currentPos = positions.get(cells[1])!;
494
+ const result = findNextFocusableCell(
495
+ grid,
496
+ currentPos,
497
+ { x: 1, y: 0 },
498
+ cells[1],
499
+ );
500
+
501
+ expect(result).toBeNull();
502
+ });
503
+
504
+ test("should find next focusable cell downward", () => {
505
+ const table = createTable(`
506
+ <table>
507
+ <tr><td><button>A</button></td></tr>
508
+ <tr><td><button>B</button></td></tr>
509
+ </table>
510
+ `);
511
+
512
+ const { grid, positions } = buildTableGridMap(table);
513
+ const cells = Array.from(table.querySelectorAll("td"));
514
+
515
+ const currentPos = positions.get(cells[0])!;
516
+ const result = findNextFocusableCell(
517
+ grid,
518
+ currentPos,
519
+ { x: 0, y: 1 },
520
+ cells[0],
521
+ );
522
+
523
+ expect(result).toBe(cells[1]);
524
+ });
525
+ });
526
+
527
+ describe("findFirstCellInRow", () => {
528
+ test("should find first focusable cell in row", () => {
529
+ const table = createTable(`
530
+ <table>
531
+ <tr>
532
+ <td><button>A</button></td>
533
+ <td><button>B</button></td>
534
+ <td><button>C</button></td>
535
+ </tr>
536
+ </table>
537
+ `);
538
+
539
+ const { grid } = buildTableGridMap(table);
540
+ const cells = Array.from(table.querySelectorAll("td"));
541
+
542
+ const result = findFirstCellInRow(grid, 0);
543
+
544
+ expect(result).toBe(cells[0]);
545
+ });
546
+ });
547
+
548
+ describe("findLastCellInRow", () => {
549
+ test("should find last focusable cell in row", () => {
550
+ const table = createTable(`
551
+ <table>
552
+ <tr>
553
+ <td><button>A</button></td>
554
+ <td><button>B</button></td>
555
+ <td><button>C</button></td>
556
+ </tr>
557
+ </table>
558
+ `);
559
+
560
+ const { grid } = buildTableGridMap(table);
561
+ const cells = Array.from(table.querySelectorAll("td"));
562
+
563
+ const result = findLastCellInRow(grid, 0);
564
+
565
+ expect(result).toBe(cells[2]);
566
+ });
567
+ });
568
+
569
+ describe("findFirstCell", () => {
570
+ test("should find first focusable cell in table", () => {
571
+ const table = createTable(`
572
+ <table>
573
+ <tr>
574
+ <td><button>A</button></td>
575
+ <td><button>B</button></td>
576
+ </tr>
577
+ <tr>
578
+ <td><button>C</button></td>
579
+ <td><button>D</button></td>
580
+ </tr>
581
+ </table>
582
+ `);
583
+
584
+ const { grid } = buildTableGridMap(table);
585
+ const cells = Array.from(table.querySelectorAll("td"));
586
+
587
+ const result = findFirstCell(grid);
588
+
589
+ expect(result).toBe(cells[0]);
590
+ });
591
+
592
+ test("should skip non-focusable cells", () => {
593
+ const table = createTable(`
594
+ <table>
595
+ <tr>
596
+ <td style="visibility:hidden;">A</td>
597
+ <td>B</td>
598
+ </tr>
599
+ <tr>
600
+ <td><button>C</button></td>
601
+ <td><button>D</button></td>
602
+ </tr>
603
+ </table>
604
+ `);
605
+
606
+ const { grid } = buildTableGridMap(table);
607
+ const cells = Array.from(table.querySelectorAll("td"));
608
+
609
+ const result = findFirstCell(grid);
610
+
611
+ expect(result).toBe(cells[1]);
612
+ });
613
+ });
614
+
615
+ describe("findLastCell", () => {
616
+ test("should find last focusable cell in table", () => {
617
+ const table = createTable(`
618
+ <table>
619
+ <tr>
620
+ <td><button>A</button></td>
621
+ <td><button>B</button></td>
622
+ </tr>
623
+ <tr>
624
+ <td><button>C</button></td>
625
+ <td><button>D</button></td>
626
+ </tr>
627
+ </table>
628
+ `);
629
+
630
+ const { grid } = buildTableGridMap(table);
631
+ const cells = Array.from(table.querySelectorAll("td"));
632
+
633
+ const result = findLastCell(grid);
634
+
635
+ expect(result).toBe(cells[3]);
636
+ });
637
+
638
+ test("should skip non-focusable cells", () => {
639
+ const table = createTable(`
640
+ <table>
641
+ <tr>
642
+ <td><button>A</button></td>
643
+ <td><button>B</button></td>
644
+ </tr>
645
+ <tr>
646
+ <td><button>C</button></td>
647
+ <td style="visibility:hidden;">D</td>
648
+ </tr>
649
+ </table>
650
+ `);
651
+
652
+ const { grid } = buildTableGridMap(table);
653
+ const cells = Array.from(table.querySelectorAll("td"));
654
+
655
+ const result = findLastCell(grid);
656
+
657
+ expect(result).toBe(cells[2]);
658
+ });
659
+ });