@jsenv/dom 0.6.1 → 0.7.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 (109) hide show
  1. package/dist/jsenv_dom.js +259 -314
  2. package/package.json +2 -4
  3. package/index.js +0 -124
  4. package/src/attr/add_attribute_effect.js +0 -93
  5. package/src/attr/attributes.js +0 -32
  6. package/src/color/color_constrast.js +0 -69
  7. package/src/color/color_parsing.js +0 -319
  8. package/src/color/color_scheme.js +0 -28
  9. package/src/color/pick_light_or_dark.js +0 -34
  10. package/src/color/resolve_css_color.js +0 -60
  11. package/src/demos/3_columns_resize_demo.html +0 -84
  12. package/src/demos/3_rows_resize_demo.html +0 -89
  13. package/src/demos/aside_and_main_demo.html +0 -93
  14. package/src/demos/coordinates_demo.html +0 -450
  15. package/src/demos/document_autoscroll_demo.html +0 -517
  16. package/src/demos/drag_gesture_constraints_demo.html +0 -701
  17. package/src/demos/drag_gesture_demo.html +0 -1047
  18. package/src/demos/drag_gesture_element_to_impact_demo.html +0 -445
  19. package/src/demos/drag_reference_element_demo.html +0 -480
  20. package/src/demos/flex_details_set_demo.html +0 -302
  21. package/src/demos/flex_details_set_demo_2.html +0 -315
  22. package/src/demos/visible_rect_demo.html +0 -525
  23. package/src/element_signature.js +0 -100
  24. package/src/interaction/drag/constraint_feedback_line.js +0 -92
  25. package/src/interaction/drag/drag_constraint.js +0 -659
  26. package/src/interaction/drag/drag_debug_markers.js +0 -635
  27. package/src/interaction/drag/drag_element_positioner.js +0 -382
  28. package/src/interaction/drag/drag_gesture.js +0 -566
  29. package/src/interaction/drag/drag_resize_demo.html +0 -571
  30. package/src/interaction/drag/drag_to_move.js +0 -301
  31. package/src/interaction/drag/drag_to_resize_gesture.js +0 -68
  32. package/src/interaction/drag/drop_target_detection.js +0 -148
  33. package/src/interaction/drag/sticky_frontiers.js +0 -160
  34. package/src/interaction/event_marker.js +0 -14
  35. package/src/interaction/focus/active_element.js +0 -33
  36. package/src/interaction/focus/arrow_navigation.js +0 -599
  37. package/src/interaction/focus/element_is_focusable.js +0 -57
  38. package/src/interaction/focus/element_visibility.js +0 -111
  39. package/src/interaction/focus/find_focusable.js +0 -21
  40. package/src/interaction/focus/focus_group.js +0 -91
  41. package/src/interaction/focus/focus_group_registry.js +0 -12
  42. package/src/interaction/focus/focus_nav.js +0 -12
  43. package/src/interaction/focus/focus_nav_event_marker.js +0 -14
  44. package/src/interaction/focus/focus_trap.js +0 -105
  45. package/src/interaction/focus/tab_navigation.js +0 -128
  46. package/src/interaction/focus/tests/focus_group_skip_tab_test.html +0 -206
  47. package/src/interaction/focus/tests/tree_focus_test.html +0 -304
  48. package/src/interaction/focus/tests/tree_focus_test.jsx +0 -261
  49. package/src/interaction/focus/tests/tree_focus_test_preact.html +0 -13
  50. package/src/interaction/isolate_interactions.js +0 -161
  51. package/src/interaction/keyboard.js +0 -26
  52. package/src/interaction/scroll/capture_scroll.js +0 -47
  53. package/src/interaction/scroll/is_scrollable.js +0 -159
  54. package/src/interaction/scroll/scroll_container.js +0 -110
  55. package/src/interaction/scroll/scroll_trap.js +0 -44
  56. package/src/interaction/scroll/scrollbar_size.js +0 -20
  57. package/src/interaction/scroll/wheel_through.js +0 -138
  58. package/src/iterable_weak_set.js +0 -66
  59. package/src/position/dom_coords.js +0 -340
  60. package/src/position/offset_parent.js +0 -15
  61. package/src/position/position_fixed.js +0 -15
  62. package/src/position/position_sticky.js +0 -213
  63. package/src/position/sticky_rect.js +0 -79
  64. package/src/position/visible_rect.js +0 -486
  65. package/src/pub_sub.js +0 -31
  66. package/src/size/can_take_size.js +0 -11
  67. package/src/size/details_content_full_height.js +0 -63
  68. package/src/size/flex_details_set.js +0 -974
  69. package/src/size/get_available_height.js +0 -22
  70. package/src/size/get_available_width.js +0 -22
  71. package/src/size/get_border_sizes.js +0 -14
  72. package/src/size/get_height.js +0 -4
  73. package/src/size/get_inner_height.js +0 -15
  74. package/src/size/get_inner_width.js +0 -15
  75. package/src/size/get_margin_sizes.js +0 -10
  76. package/src/size/get_max_height.js +0 -57
  77. package/src/size/get_max_width.js +0 -47
  78. package/src/size/get_min_height.js +0 -14
  79. package/src/size/get_min_width.js +0 -14
  80. package/src/size/get_padding_sizes.js +0 -10
  81. package/src/size/get_width.js +0 -4
  82. package/src/size/hooks/use_available_height.js +0 -27
  83. package/src/size/hooks/use_available_width.js +0 -27
  84. package/src/size/hooks/use_max_height.js +0 -10
  85. package/src/size/hooks/use_max_width.js +0 -10
  86. package/src/size/hooks/use_resize_status.js +0 -62
  87. package/src/size/resize.js +0 -695
  88. package/src/size/resolve_css_size.js +0 -32
  89. package/src/style/dom_styles.js +0 -97
  90. package/src/style/style_composition.js +0 -121
  91. package/src/style/style_controller.js +0 -345
  92. package/src/style/style_default.js +0 -153
  93. package/src/style/style_default_demo.html +0 -128
  94. package/src/style/style_parsing.js +0 -375
  95. package/src/transition/demos/animation_resumption_test.xhtml +0 -500
  96. package/src/transition/demos/height_toggle_test.xhtml +0 -515
  97. package/src/transition/dom_transition.js +0 -254
  98. package/src/transition/easing.js +0 -48
  99. package/src/transition/group_transition.js +0 -261
  100. package/src/transition/transform_style_parser.js +0 -32
  101. package/src/transition/transition_playback.js +0 -366
  102. package/src/transition/transition_timeline.js +0 -79
  103. package/src/traversal.js +0 -247
  104. package/src/ui_transition/demos/content_states_transition_demo.html +0 -628
  105. package/src/ui_transition/demos/smooth_height_transition_demo.html +0 -149
  106. package/src/ui_transition/demos/transition_testing.html +0 -354
  107. package/src/ui_transition/ui_transition.js +0 -1470
  108. package/src/utils.js +0 -69
  109. package/src/value_effect.js +0 -35
@@ -1,33 +0,0 @@
1
- import { effect, signal } from "@preact/signals";
2
-
3
- export const activeElementSignal = signal(document.activeElement);
4
-
5
- document.addEventListener(
6
- "focus",
7
- () => {
8
- activeElementSignal.value = document.activeElement;
9
- },
10
- { capture: true },
11
- );
12
- // When clicking on document there is no "focus" event dispatched on the document
13
- // We can detect that with "blur" event when relatedTarget is null
14
- document.addEventListener(
15
- "blur",
16
- (e) => {
17
- if (!e.relatedTarget) {
18
- activeElementSignal.value = document.activeElement;
19
- }
20
- },
21
- { capture: true },
22
- );
23
-
24
- export const useActiveElement = () => {
25
- return activeElementSignal.value;
26
- };
27
- export const addActiveElementEffect = (callback) => {
28
- const remove = effect(() => {
29
- const activeElement = activeElementSignal.value;
30
- callback(activeElement);
31
- });
32
- return remove;
33
- };
@@ -1,599 +0,0 @@
1
- import {
2
- findAfter,
3
- findBefore,
4
- findDescendant,
5
- findLastDescendant,
6
- } from "../../traversal.js";
7
- import { canInterceptKeys } from "../keyboard.js";
8
- import { elementIsFocusable } from "./element_is_focusable.js";
9
- import { getFocusGroup } from "./focus_group_registry.js";
10
- import { markFocusNav } from "./focus_nav_event_marker.js";
11
-
12
- const DEBUG = false;
13
-
14
- export const performArrowNavigation = (
15
- event,
16
- element,
17
- { direction = "both", loop, name } = {},
18
- ) => {
19
- if (!canInterceptKeys(event)) {
20
- return false;
21
- }
22
- const activeElement = document.activeElement;
23
- if (activeElement.hasAttribute("data-focusnav") === "none") {
24
- // no need to prevent default here (arrow don't move focus by default in a focus group)
25
- // (and it would prevent scroll via keyboard that we might want here)
26
- return true;
27
- }
28
-
29
- const onTargetToFocus = (targetToFocus) => {
30
- console.debug(
31
- `Arrow navigation: ${event.key} from`,
32
- activeElement,
33
- "to",
34
- targetToFocus,
35
- );
36
- event.preventDefault();
37
- markFocusNav(event);
38
- targetToFocus.focus();
39
- };
40
-
41
- // Grid navigation: we support only TABLE element for now
42
- // A role="table" or an element with display: table could be used too but for now we need only TABLE support
43
- if (element.tagName === "TABLE") {
44
- const targetInGrid = getTargetInTableFocusGroup(event, element, { loop });
45
- if (!targetInGrid) {
46
- return false;
47
- }
48
- onTargetToFocus(targetInGrid);
49
- return true;
50
- }
51
-
52
- const targetInLinearGroup = getTargetInLinearFocusGroup(event, element, {
53
- direction,
54
- loop,
55
- name,
56
- });
57
- if (!targetInLinearGroup) {
58
- return false;
59
- }
60
- onTargetToFocus(targetInLinearGroup);
61
- return true;
62
- };
63
-
64
- const getTargetInLinearFocusGroup = (
65
- event,
66
- element,
67
- { direction, loop, name },
68
- ) => {
69
- const activeElement = document.activeElement;
70
-
71
- // Check for Cmd/Ctrl + arrow keys for jumping to start/end of linear group
72
- const isJumpToEnd = event.metaKey || event.ctrlKey;
73
-
74
- if (isJumpToEnd) {
75
- return getJumpToEndTargetLinear(event, element, direction);
76
- }
77
-
78
- const isForward = isForwardArrow(event, direction);
79
-
80
- // Arrow Left/Up: move to previous focusable element in group
81
- backward: {
82
- if (!isBackwardArrow(event, direction)) {
83
- break backward;
84
- }
85
- const previousElement = findBefore(activeElement, elementIsFocusable, {
86
- root: element,
87
- });
88
- if (previousElement) {
89
- return previousElement;
90
- }
91
- const ancestorTarget = delegateArrowNavigation(event, element, {
92
- name,
93
- });
94
- if (ancestorTarget) {
95
- return ancestorTarget;
96
- }
97
- if (loop) {
98
- const lastFocusableElement = findLastDescendant(
99
- element,
100
- elementIsFocusable,
101
- );
102
- if (lastFocusableElement) {
103
- return lastFocusableElement;
104
- }
105
- }
106
- return null;
107
- }
108
-
109
- // Arrow Right/Down: move to next focusable element in group
110
- forward: {
111
- if (!isForward) {
112
- break forward;
113
- }
114
- const nextElement = findAfter(activeElement, elementIsFocusable, {
115
- root: element,
116
- });
117
- if (nextElement) {
118
- return nextElement;
119
- }
120
- const ancestorTarget = delegateArrowNavigation(event, element, {
121
- name,
122
- });
123
- if (ancestorTarget) {
124
- return ancestorTarget;
125
- }
126
- if (loop) {
127
- // No next element, wrap to first focusable in group
128
- const firstFocusableElement = findDescendant(element, elementIsFocusable);
129
- if (firstFocusableElement) {
130
- return firstFocusableElement;
131
- }
132
- }
133
- return null;
134
- }
135
-
136
- return null;
137
- };
138
- // Find parent focus group with the same name and try delegation
139
- const delegateArrowNavigation = (event, currentElement, { name }) => {
140
- let ancestorElement = currentElement.parentElement;
141
- while (ancestorElement) {
142
- const ancestorFocusGroup = getFocusGroup(ancestorElement);
143
- if (!ancestorFocusGroup) {
144
- ancestorElement = ancestorElement.parentElement;
145
- continue;
146
- }
147
-
148
- // Check if groups should delegate to each other
149
- const shouldDelegate =
150
- name === undefined && ancestorFocusGroup.name === undefined
151
- ? true // Both unnamed - delegate based on ancestor relationship
152
- : ancestorFocusGroup.name === name; // Both have same explicit name
153
-
154
- if (shouldDelegate) {
155
- if (DEBUG) {
156
- console.debug(
157
- `Delegating navigation to parent focus group:`,
158
- ancestorElement,
159
- name === undefined ? "(unnamed group)" : `(name: ${name})`,
160
- );
161
- }
162
- // Try navigation in parent focus group
163
- return getTargetInLinearFocusGroup(event, ancestorElement, {
164
- direction: ancestorFocusGroup.direction,
165
- loop: ancestorFocusGroup.loop,
166
- name: ancestorFocusGroup.name,
167
- });
168
- }
169
- }
170
- return null;
171
- };
172
-
173
- // Handle Cmd/Ctrl + arrow keys for linear focus groups to jump to start/end
174
- const getJumpToEndTargetLinear = (event, element, direction) => {
175
- // Check if this arrow key is valid for the given direction
176
- if (!isForwardArrow(event, direction) && !isBackwardArrow(event, direction)) {
177
- return null;
178
- }
179
-
180
- if (isBackwardArrow(event, direction)) {
181
- // Jump to first focusable element in the group
182
- return findDescendant(element, elementIsFocusable);
183
- }
184
-
185
- if (isForwardArrow(event, direction)) {
186
- // Jump to last focusable element in the group
187
- return findLastDescendant(element, elementIsFocusable);
188
- }
189
-
190
- return null;
191
- };
192
-
193
- const isBackwardArrow = (event, direction = "both") => {
194
- const backwardKeys = {
195
- both: ["ArrowLeft", "ArrowUp"],
196
- vertical: ["ArrowUp"],
197
- horizontal: ["ArrowLeft"],
198
- };
199
- return backwardKeys[direction]?.includes(event.key) ?? false;
200
- };
201
- const isForwardArrow = (event, direction = "both") => {
202
- const forwardKeys = {
203
- both: ["ArrowRight", "ArrowDown"],
204
- vertical: ["ArrowDown"],
205
- horizontal: ["ArrowRight"],
206
- };
207
- return forwardKeys[direction]?.includes(event.key) ?? false;
208
- };
209
-
210
- // Handle arrow navigation inside an HTMLTableElement as a grid.
211
- // Moves focus to adjacent cell in the direction of the arrow key.
212
- const getTargetInTableFocusGroup = (event, table, { loop }) => {
213
- const arrowKey = event.key;
214
-
215
- // Only handle arrow keys
216
- if (
217
- arrowKey !== "ArrowRight" &&
218
- arrowKey !== "ArrowLeft" &&
219
- arrowKey !== "ArrowUp" &&
220
- arrowKey !== "ArrowDown"
221
- ) {
222
- return null;
223
- }
224
-
225
- const focusedElement = document.activeElement;
226
- const currentCell = focusedElement?.closest?.("td,th");
227
-
228
- // If we're not currently in a table cell, try to focus the first focusable element in the table
229
- if (!currentCell || !table.contains(currentCell)) {
230
- return findDescendant(table, elementIsFocusable) || null;
231
- }
232
-
233
- // Get the current position in the table grid
234
- const currentRow = currentCell.parentElement; // tr element
235
- const allRows = Array.from(table.rows);
236
- const currentRowIndex = /** @type {HTMLTableRowElement} */ (currentRow)
237
- .rowIndex;
238
- const currentColumnIndex = /** @type {HTMLTableCellElement} */ (currentCell)
239
- .cellIndex;
240
-
241
- // Check for Cmd/Ctrl + arrow keys for jumping to end of row/column
242
- const isJumpToEnd = event.metaKey || event.ctrlKey;
243
-
244
- if (isJumpToEnd) {
245
- return getJumpToEndTarget(
246
- arrowKey,
247
- allRows,
248
- currentRowIndex,
249
- currentColumnIndex,
250
- );
251
- }
252
-
253
- // Create an iterator that will scan through cells in the arrow direction
254
- // until it finds one with a focusable element inside
255
- const candidateCells = createTableCellIterator(arrowKey, allRows, {
256
- startRow: currentRowIndex,
257
- startColumn: currentColumnIndex,
258
- originalColumn: currentColumnIndex, // Used to maintain column alignment for vertical moves
259
- loopMode: normalizeLoop(loop),
260
- });
261
-
262
- // Find the first cell that is itself focusable
263
- for (const candidateCell of candidateCells) {
264
- if (elementIsFocusable(candidateCell)) {
265
- return candidateCell;
266
- }
267
- }
268
-
269
- return null; // No focusable cell found
270
- };
271
-
272
- // Handle Cmd/Ctrl + arrow keys to jump to the end of row/column
273
- const getJumpToEndTarget = (
274
- arrowKey,
275
- allRows,
276
- currentRowIndex,
277
- currentColumnIndex,
278
- ) => {
279
- if (arrowKey === "ArrowRight") {
280
- // Jump to last focusable cell in current row
281
- const currentRow = allRows[currentRowIndex];
282
- if (!currentRow) return null;
283
-
284
- // Start from the last cell and work backwards to find focusable
285
- const cells = Array.from(currentRow.cells);
286
- for (let i = cells.length - 1; i >= 0; i--) {
287
- const cell = cells[i];
288
- if (elementIsFocusable(cell)) {
289
- return cell;
290
- }
291
- }
292
- return null;
293
- }
294
-
295
- if (arrowKey === "ArrowLeft") {
296
- // Jump to first focusable cell in current row
297
- const currentRow = allRows[currentRowIndex];
298
- if (!currentRow) return null;
299
-
300
- const cells = Array.from(currentRow.cells);
301
- for (const cell of cells) {
302
- if (elementIsFocusable(cell)) {
303
- return cell;
304
- }
305
- }
306
- return null;
307
- }
308
-
309
- if (arrowKey === "ArrowDown") {
310
- // Jump to last focusable cell in current column
311
- for (let rowIndex = allRows.length - 1; rowIndex >= 0; rowIndex--) {
312
- const row = allRows[rowIndex];
313
- const cell = row?.cells?.[currentColumnIndex];
314
- if (cell && elementIsFocusable(cell)) {
315
- return cell;
316
- }
317
- }
318
- return null;
319
- }
320
-
321
- if (arrowKey === "ArrowUp") {
322
- // Jump to first focusable cell in current column
323
- for (let rowIndex = 0; rowIndex < allRows.length; rowIndex++) {
324
- const row = allRows[rowIndex];
325
- const cell = row?.cells?.[currentColumnIndex];
326
- if (cell && elementIsFocusable(cell)) {
327
- return cell;
328
- }
329
- }
330
- return null;
331
- }
332
-
333
- return null;
334
- };
335
-
336
- // Create an iterator that yields table cells in the direction of arrow key movement.
337
- // This scans through cells until it finds one with a focusable element or completes a full loop.
338
- const createTableCellIterator = function* (
339
- arrowKey,
340
- allRows,
341
- { startRow, startColumn, originalColumn, loopMode },
342
- ) {
343
- if (allRows.length === 0) {
344
- return; // No rows to navigate
345
- }
346
-
347
- // Keep track of which column we should prefer for vertical movements
348
- // This helps maintain column alignment when moving up/down through rows of different lengths
349
- let preferredColumn = originalColumn;
350
-
351
- const normalizedLoopMode = normalizeLoop(loopMode);
352
-
353
- // Helper function to calculate the next position based on current position and arrow key
354
- const calculateNextPosition = (currentRow, currentColumn) =>
355
- getNextTablePosition(
356
- arrowKey,
357
- allRows,
358
- currentRow,
359
- currentColumn,
360
- preferredColumn,
361
- normalizedLoopMode,
362
- );
363
-
364
- // Start by calculating the first position to move to
365
- let nextPosition = calculateNextPosition(startRow, startColumn);
366
- if (!nextPosition) {
367
- return; // Cannot move in this direction (no looping enabled)
368
- }
369
-
370
- // Keep track of our actual starting position to detect when we've completed a full loop
371
- const actualStartingPosition = `${startRow}:${startColumn}`;
372
-
373
- while (true) {
374
- const [nextColumn, nextRow] = nextPosition; // Destructure [column, row]
375
- const targetRow = allRows[nextRow];
376
- const targetCell = targetRow?.cells?.[nextColumn];
377
-
378
- // Yield the cell if it exists
379
- if (targetCell) {
380
- yield targetCell;
381
- }
382
-
383
- // Update our preferred column based on movement:
384
- // - For horizontal moves, update to current column
385
- // - For vertical moves in flow mode at boundaries, advance to next/previous column
386
- if (arrowKey === "ArrowRight" || arrowKey === "ArrowLeft") {
387
- preferredColumn = nextColumn;
388
- } else if (arrowKey === "ArrowDown") {
389
- const isAtBottomRow = nextRow === allRows.length - 1;
390
- if (isAtBottomRow && normalizedLoopMode === "flow") {
391
- // Moving down from bottom row in flow mode: advance to next column
392
- const maxColumns = getMaxColumns(allRows);
393
- preferredColumn = preferredColumn + 1;
394
- if (preferredColumn >= maxColumns) {
395
- preferredColumn = 0; // Wrap to first column
396
- }
397
- }
398
- } else if (arrowKey === "ArrowUp") {
399
- const isAtTopRow = nextRow === 0;
400
- if (isAtTopRow && normalizedLoopMode === "flow") {
401
- // Moving up from top row in flow mode: go to previous column
402
- const maxColumns = getMaxColumns(allRows);
403
- if (preferredColumn === 0) {
404
- preferredColumn = maxColumns - 1; // Wrap to last column
405
- } else {
406
- preferredColumn = preferredColumn - 1;
407
- }
408
- }
409
- }
410
-
411
- // Calculate where to move next
412
- nextPosition = calculateNextPosition(nextRow, nextColumn);
413
- if (!nextPosition) {
414
- return; // Hit a boundary with no looping
415
- }
416
-
417
- // Check if we've completed a full loop by returning to our actual starting position
418
- const currentPositionKey = `${nextRow}:${nextColumn}`;
419
- if (currentPositionKey === actualStartingPosition) {
420
- return; // We've gone full circle back to where we started
421
- }
422
- }
423
- };
424
-
425
- // Normalize loop option to a mode string or false
426
- const normalizeLoop = (loop) => {
427
- if (loop === true) return "wrap";
428
- if (loop === "wrap") return "wrap";
429
- if (loop === "flow") return "flow";
430
- return false;
431
- };
432
-
433
- const getMaxColumns = (rows) =>
434
- rows.reduce((max, r) => Math.max(max, r?.cells?.length || 0), 0);
435
-
436
- // Calculate the next row and column position when moving in a table with arrow keys.
437
- // Returns [column, row] for the next position, or null if movement is not possible.
438
- const getNextTablePosition = (
439
- arrowKey,
440
- allRows,
441
- currentRow,
442
- currentColumn,
443
- preferredColumn, // Used for vertical movement to maintain column alignment
444
- loopMode,
445
- ) => {
446
- if (arrowKey === "ArrowRight") {
447
- const currentRowLength = allRows[currentRow]?.cells?.length || 0;
448
- const nextColumn = currentColumn + 1;
449
-
450
- // Can we move right within the same row?
451
- if (nextColumn < currentRowLength) {
452
- return [nextColumn, currentRow]; // [column, row]
453
- }
454
-
455
- // We're at the end of the row - handle boundary behavior
456
- if (loopMode === "flow") {
457
- // Flow mode: move to first cell of next row (wrap to top if at bottom)
458
- let nextRow = currentRow + 1;
459
- if (nextRow >= allRows.length) {
460
- nextRow = 0; // Wrap to first row
461
- }
462
- return [0, nextRow]; // [column, row]
463
- }
464
-
465
- if (loopMode === "wrap") {
466
- // Wrap mode: stay in same row, wrap to first column
467
- return [0, currentRow]; // [column, row]
468
- }
469
-
470
- // No looping: can't move
471
- return null;
472
- }
473
-
474
- if (arrowKey === "ArrowLeft") {
475
- const previousColumn = currentColumn - 1;
476
-
477
- // Can we move left within the same row?
478
- if (previousColumn >= 0) {
479
- return [previousColumn, currentRow]; // [column, row]
480
- }
481
-
482
- // We're at the beginning of the row - handle boundary behavior
483
- if (loopMode === "flow") {
484
- // Flow mode: move to last cell of previous row (wrap to bottom if at top)
485
- let previousRow = currentRow - 1;
486
- if (previousRow < 0) {
487
- previousRow = allRows.length - 1; // Wrap to last row
488
- }
489
- const previousRowLength = allRows[previousRow]?.cells?.length || 0;
490
- const lastColumnInPreviousRow = Math.max(0, previousRowLength - 1);
491
- return [lastColumnInPreviousRow, previousRow]; // [column, row]
492
- }
493
-
494
- if (loopMode === "wrap") {
495
- // Wrap mode: stay in same row, wrap to last column
496
- const currentRowLength = allRows[currentRow]?.cells?.length || 0;
497
- const lastColumnInCurrentRow = Math.max(0, currentRowLength - 1);
498
- return [lastColumnInCurrentRow, currentRow]; // [column, row]
499
- }
500
-
501
- // No looping: can't move
502
- return null;
503
- }
504
-
505
- if (arrowKey === "ArrowDown") {
506
- const nextRow = currentRow + 1;
507
-
508
- // Can we move down within the table?
509
- if (nextRow < allRows.length) {
510
- const nextRowLength = allRows[nextRow]?.cells?.length || 0;
511
- // Try to maintain the preferred column, but clamp to row length
512
- const targetColumn = Math.min(
513
- preferredColumn,
514
- Math.max(0, nextRowLength - 1),
515
- );
516
- return [targetColumn, nextRow]; // [column, row]
517
- }
518
-
519
- // We're at the bottom row - handle boundary behavior
520
- if (loopMode === "flow") {
521
- // Flow mode: advance to next column and go to top row
522
- const maxColumns = Math.max(1, getMaxColumns(allRows));
523
- let nextColumnInFlow = currentColumn + 1;
524
- if (nextColumnInFlow >= maxColumns) {
525
- nextColumnInFlow = 0; // Wrap to first column
526
- }
527
- const topRowLength = allRows[0]?.cells?.length || 0;
528
- const clampedColumn = Math.min(
529
- nextColumnInFlow,
530
- Math.max(0, topRowLength - 1),
531
- );
532
- return [clampedColumn, 0]; // [column, row]
533
- }
534
-
535
- if (loopMode === "wrap") {
536
- // Wrap mode: go to top row, maintaining preferred column
537
- const topRowLength = allRows[0]?.cells?.length || 0;
538
- const targetColumn = Math.min(
539
- preferredColumn,
540
- Math.max(0, topRowLength - 1),
541
- );
542
- return [targetColumn, 0]; // [column, row]
543
- }
544
-
545
- // No looping: can't move
546
- return null;
547
- }
548
-
549
- if (arrowKey === "ArrowUp") {
550
- const previousRow = currentRow - 1;
551
-
552
- // Can we move up within the table?
553
- if (previousRow >= 0) {
554
- const previousRowLength = allRows[previousRow]?.cells?.length || 0;
555
- // Try to maintain the preferred column, but clamp to row length
556
- const targetColumn = Math.min(
557
- preferredColumn,
558
- Math.max(0, previousRowLength - 1),
559
- );
560
- return [targetColumn, previousRow]; // [column, row]
561
- }
562
-
563
- // We're at the top row - handle boundary behavior
564
- if (loopMode === "flow") {
565
- // Flow mode: go to previous column and move to bottom row
566
- const maxColumns = Math.max(1, getMaxColumns(allRows));
567
- let previousColumnInFlow;
568
- if (currentColumn === 0) {
569
- previousColumnInFlow = maxColumns - 1; // Wrap to last column
570
- } else {
571
- previousColumnInFlow = currentColumn - 1;
572
- }
573
- const bottomRowIndex = allRows.length - 1;
574
- const bottomRowLength = allRows[bottomRowIndex]?.cells?.length || 0;
575
- const clampedColumn = Math.min(
576
- previousColumnInFlow,
577
- Math.max(0, bottomRowLength - 1),
578
- );
579
- return [clampedColumn, bottomRowIndex]; // [column, row]
580
- }
581
-
582
- if (loopMode === "wrap") {
583
- // Wrap mode: go to bottom row, maintaining preferred column
584
- const bottomRowIndex = allRows.length - 1;
585
- const bottomRowLength = allRows[bottomRowIndex]?.cells?.length || 0;
586
- const targetColumn = Math.min(
587
- preferredColumn,
588
- Math.max(0, bottomRowLength - 1),
589
- );
590
- return [targetColumn, bottomRowIndex]; // [column, row]
591
- }
592
-
593
- // No looping: can't move
594
- return null;
595
- }
596
-
597
- // Unknown arrow key
598
- return null;
599
- };
@@ -1,57 +0,0 @@
1
- import { elementIsVisibleForFocus } from "./element_visibility.js";
2
-
3
- export const elementIsFocusable = (node) => {
4
- // only element node can be focused, document, textNodes etc cannot
5
- if (node.nodeType !== 1) {
6
- return false;
7
- }
8
- if (!canInteract(node)) {
9
- return false;
10
- }
11
- const nodeName = node.nodeName.toLowerCase();
12
- if (nodeName === "input") {
13
- if (node.type === "hidden") {
14
- return false;
15
- }
16
- return elementIsVisibleForFocus(node);
17
- }
18
- if (
19
- ["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
20
- -1
21
- ) {
22
- return elementIsVisibleForFocus(node);
23
- }
24
- if (["a", "area"].indexOf(nodeName) > -1) {
25
- if (node.hasAttribute("href") === false) {
26
- return false;
27
- }
28
- return elementIsVisibleForFocus(node);
29
- }
30
- if (["audio", "video"].indexOf(nodeName) > -1) {
31
- if (node.hasAttribute("controls") === false) {
32
- return false;
33
- }
34
- return elementIsVisibleForFocus(node);
35
- }
36
- if (nodeName === "summary") {
37
- return elementIsVisibleForFocus(node);
38
- }
39
- if (node.hasAttribute("tabindex") || node.hasAttribute("tabIndex")) {
40
- return elementIsVisibleForFocus(node);
41
- }
42
- if (node.hasAttribute("draggable")) {
43
- return elementIsVisibleForFocus(node);
44
- }
45
- return false;
46
- };
47
-
48
- const canInteract = (element) => {
49
- if (element.disabled) {
50
- return false;
51
- }
52
- if (element.hasAttribute("inert")) {
53
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert
54
- return false;
55
- }
56
- return true;
57
- };