@jsenv/dom 0.6.0 → 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.
- package/dist/jsenv_dom.js +262 -330
- package/package.json +2 -4
- package/index.js +0 -124
- package/src/attr/add_attribute_effect.js +0 -93
- package/src/attr/attributes.js +0 -32
- package/src/color/color_constrast.js +0 -69
- package/src/color/color_parsing.js +0 -319
- package/src/color/color_scheme.js +0 -28
- package/src/color/pick_light_or_dark.js +0 -34
- package/src/color/resolve_css_color.js +0 -60
- package/src/demos/3_columns_resize_demo.html +0 -84
- package/src/demos/3_rows_resize_demo.html +0 -89
- package/src/demos/aside_and_main_demo.html +0 -93
- package/src/demos/coordinates_demo.html +0 -450
- package/src/demos/document_autoscroll_demo.html +0 -517
- package/src/demos/drag_gesture_constraints_demo.html +0 -701
- package/src/demos/drag_gesture_demo.html +0 -1047
- package/src/demos/drag_gesture_element_to_impact_demo.html +0 -445
- package/src/demos/drag_reference_element_demo.html +0 -480
- package/src/demos/flex_details_set_demo.html +0 -302
- package/src/demos/flex_details_set_demo_2.html +0 -315
- package/src/demos/visible_rect_demo.html +0 -525
- package/src/element_signature.js +0 -100
- package/src/interaction/drag/constraint_feedback_line.js +0 -92
- package/src/interaction/drag/drag_constraint.js +0 -659
- package/src/interaction/drag/drag_debug_markers.js +0 -635
- package/src/interaction/drag/drag_element_positioner.js +0 -382
- package/src/interaction/drag/drag_gesture.js +0 -566
- package/src/interaction/drag/drag_resize_demo.html +0 -571
- package/src/interaction/drag/drag_to_move.js +0 -301
- package/src/interaction/drag/drag_to_resize_gesture.js +0 -68
- package/src/interaction/drag/drop_target_detection.js +0 -148
- package/src/interaction/drag/sticky_frontiers.js +0 -160
- package/src/interaction/event_marker.js +0 -14
- package/src/interaction/focus/active_element.js +0 -33
- package/src/interaction/focus/arrow_navigation.js +0 -599
- package/src/interaction/focus/element_is_focusable.js +0 -57
- package/src/interaction/focus/element_visibility.js +0 -111
- package/src/interaction/focus/find_focusable.js +0 -21
- package/src/interaction/focus/focus_group.js +0 -91
- package/src/interaction/focus/focus_group_registry.js +0 -12
- package/src/interaction/focus/focus_nav.js +0 -12
- package/src/interaction/focus/focus_nav_event_marker.js +0 -14
- package/src/interaction/focus/focus_trap.js +0 -105
- package/src/interaction/focus/tab_navigation.js +0 -128
- package/src/interaction/focus/tests/focus_group_skip_tab_test.html +0 -206
- package/src/interaction/focus/tests/tree_focus_test.html +0 -304
- package/src/interaction/focus/tests/tree_focus_test.jsx +0 -261
- package/src/interaction/focus/tests/tree_focus_test_preact.html +0 -13
- package/src/interaction/isolate_interactions.js +0 -161
- package/src/interaction/keyboard.js +0 -26
- package/src/interaction/scroll/capture_scroll.js +0 -47
- package/src/interaction/scroll/is_scrollable.js +0 -159
- package/src/interaction/scroll/scroll_container.js +0 -110
- package/src/interaction/scroll/scroll_trap.js +0 -44
- package/src/interaction/scroll/scrollbar_size.js +0 -20
- package/src/interaction/scroll/wheel_through.js +0 -138
- package/src/iterable_weak_set.js +0 -66
- package/src/position/dom_coords.js +0 -340
- package/src/position/offset_parent.js +0 -15
- package/src/position/position_fixed.js +0 -15
- package/src/position/position_sticky.js +0 -213
- package/src/position/sticky_rect.js +0 -79
- package/src/position/visible_rect.js +0 -486
- package/src/pub_sub.js +0 -31
- package/src/size/can_take_size.js +0 -11
- package/src/size/details_content_full_height.js +0 -63
- package/src/size/flex_details_set.js +0 -974
- package/src/size/get_available_height.js +0 -22
- package/src/size/get_available_width.js +0 -22
- package/src/size/get_border_sizes.js +0 -14
- package/src/size/get_height.js +0 -4
- package/src/size/get_inner_height.js +0 -15
- package/src/size/get_inner_width.js +0 -15
- package/src/size/get_margin_sizes.js +0 -10
- package/src/size/get_max_height.js +0 -57
- package/src/size/get_max_width.js +0 -47
- package/src/size/get_min_height.js +0 -14
- package/src/size/get_min_width.js +0 -14
- package/src/size/get_padding_sizes.js +0 -10
- package/src/size/get_width.js +0 -4
- package/src/size/hooks/use_available_height.js +0 -27
- package/src/size/hooks/use_available_width.js +0 -27
- package/src/size/hooks/use_max_height.js +0 -10
- package/src/size/hooks/use_max_width.js +0 -10
- package/src/size/hooks/use_resize_status.js +0 -62
- package/src/size/resize.js +0 -695
- package/src/size/resolve_css_size.js +0 -32
- package/src/style/dom_styles.js +0 -97
- package/src/style/style_composition.js +0 -121
- package/src/style/style_controller.js +0 -345
- package/src/style/style_default.js +0 -153
- package/src/style/style_default_demo.html +0 -128
- package/src/style/style_parsing.js +0 -375
- package/src/transition/demos/animation_resumption_test.xhtml +0 -500
- package/src/transition/demos/height_toggle_test.xhtml +0 -515
- package/src/transition/dom_transition.js +0 -254
- package/src/transition/easing.js +0 -48
- package/src/transition/group_transition.js +0 -261
- package/src/transition/transform_style_parser.js +0 -32
- package/src/transition/transition_playback.js +0 -366
- package/src/transition/transition_timeline.js +0 -79
- package/src/traversal.js +0 -247
- package/src/ui_transition/demos/content_states_transition_demo.html +0 -628
- package/src/ui_transition/demos/smooth_height_transition_demo.html +0 -149
- package/src/ui_transition/demos/transition_testing.html +0 -354
- package/src/ui_transition/ui_transition.js +0 -1491
- package/src/utils.js +0 -69
- 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
|
-
};
|