@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,111 +0,0 @@
|
|
|
1
|
-
import { getStyle } from "../../style/dom_styles.js";
|
|
2
|
-
import {
|
|
3
|
-
elementIsDetails,
|
|
4
|
-
elementIsSummary,
|
|
5
|
-
isDocumentElement,
|
|
6
|
-
} from "../../utils.js";
|
|
7
|
-
|
|
8
|
-
export const elementIsVisibleForFocus = (node) => {
|
|
9
|
-
return getFocusVisibilityInfo(node).visible;
|
|
10
|
-
};
|
|
11
|
-
export const getFocusVisibilityInfo = (node) => {
|
|
12
|
-
if (isDocumentElement(node)) {
|
|
13
|
-
return { visible: true, reason: "is document" };
|
|
14
|
-
}
|
|
15
|
-
if (node.hasAttribute("hidden")) {
|
|
16
|
-
return { visible: false, reason: "has hidden attribute" };
|
|
17
|
-
}
|
|
18
|
-
if (getStyle(node, "visibility") === "hidden") {
|
|
19
|
-
return { visible: false, reason: "uses visiblity: hidden" };
|
|
20
|
-
}
|
|
21
|
-
if (node.tagName === "INPUT" && node.type === "hidden") {
|
|
22
|
-
return { visible: false, reason: "input type hidden" };
|
|
23
|
-
}
|
|
24
|
-
let nodeOrAncestor = node;
|
|
25
|
-
while (nodeOrAncestor) {
|
|
26
|
-
if (isDocumentElement(nodeOrAncestor)) {
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
if (getStyle(nodeOrAncestor, "display") === "none") {
|
|
30
|
-
return { visible: false, reason: "ancestor uses display: none" };
|
|
31
|
-
}
|
|
32
|
-
// Check if element is inside a closed details element
|
|
33
|
-
if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
|
|
34
|
-
// Special case: summary elements are visible even when their parent details is closed
|
|
35
|
-
// But only if this details element is the direct parent of the summary
|
|
36
|
-
if (!elementIsSummary(node) || node.parentElement !== nodeOrAncestor) {
|
|
37
|
-
return { visible: false, reason: "inside closed details element" };
|
|
38
|
-
}
|
|
39
|
-
// Continue checking ancestors
|
|
40
|
-
}
|
|
41
|
-
nodeOrAncestor = nodeOrAncestor.parentNode;
|
|
42
|
-
}
|
|
43
|
-
return { visible: true, reason: "no reason to be hidden" };
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const elementIsVisuallyVisible = (node, options = {}) => {
|
|
47
|
-
return getVisuallyVisibleInfo(node, options).visible;
|
|
48
|
-
};
|
|
49
|
-
export const getVisuallyVisibleInfo = (
|
|
50
|
-
node,
|
|
51
|
-
{ countOffscreenAsVisible = false } = {},
|
|
52
|
-
) => {
|
|
53
|
-
// First check all the focusable visibility conditions
|
|
54
|
-
const focusVisibilityInfo = getFocusVisibilityInfo(node);
|
|
55
|
-
if (!focusVisibilityInfo.visible) {
|
|
56
|
-
return focusVisibilityInfo;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Additional visual visibility checks
|
|
60
|
-
if (getStyle(node, "opacity") === "0") {
|
|
61
|
-
return { visible: false, reason: "uses opacity: 0" };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const rect = node.getBoundingClientRect();
|
|
65
|
-
if (rect.width === 0 && rect.height === 0) {
|
|
66
|
-
return { visible: false, reason: "has zero dimensions" };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check for clipping
|
|
70
|
-
const clipStyle = getStyle(node, "clip");
|
|
71
|
-
if (clipStyle && clipStyle !== "auto" && clipStyle.includes("rect(0")) {
|
|
72
|
-
return { visible: false, reason: "clipped with clip property" };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const clipPathStyle = getStyle(node, "clip-path");
|
|
76
|
-
if (clipPathStyle && clipPathStyle.includes("inset(100%")) {
|
|
77
|
-
return { visible: false, reason: "clipped with clip-path" };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check if positioned off-screen (unless option says to count as visible)
|
|
81
|
-
if (!countOffscreenAsVisible) {
|
|
82
|
-
if (
|
|
83
|
-
rect.right < 0 ||
|
|
84
|
-
rect.bottom < 0 ||
|
|
85
|
-
rect.left > window.innerWidth ||
|
|
86
|
-
rect.top > window.innerHeight
|
|
87
|
-
) {
|
|
88
|
-
return { visible: false, reason: "positioned off-screen" };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Check for transform scale(0)
|
|
93
|
-
const transformStyle = getStyle(node, "transform");
|
|
94
|
-
if (transformStyle && transformStyle.includes("scale(0")) {
|
|
95
|
-
return { visible: false, reason: "scaled to zero with transform" };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { visible: true, reason: "visually visible" };
|
|
99
|
-
};
|
|
100
|
-
export const getFirstVisuallyVisibleAncestor = (node, options = {}) => {
|
|
101
|
-
let ancestorCandidate = node.parentNode;
|
|
102
|
-
while (ancestorCandidate) {
|
|
103
|
-
const visibilityInfo = getVisuallyVisibleInfo(ancestorCandidate, options);
|
|
104
|
-
if (visibilityInfo.visible) {
|
|
105
|
-
return ancestorCandidate;
|
|
106
|
-
}
|
|
107
|
-
ancestorCandidate = ancestorCandidate.parentElement;
|
|
108
|
-
}
|
|
109
|
-
// This shouldn't happen in normal cases since document element is always visible
|
|
110
|
-
return null;
|
|
111
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { findDescendant } from "../../traversal.js";
|
|
2
|
-
import { getAssociatedElements } from "../../utils.js";
|
|
3
|
-
import { elementIsFocusable } from "./element_is_focusable.js";
|
|
4
|
-
|
|
5
|
-
export const findFocusable = (element) => {
|
|
6
|
-
const associatedElements = getAssociatedElements(element);
|
|
7
|
-
if (associatedElements) {
|
|
8
|
-
for (const associatedElement of associatedElements) {
|
|
9
|
-
const focusable = findFocusable(associatedElement);
|
|
10
|
-
if (focusable) {
|
|
11
|
-
return focusable;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
if (elementIsFocusable(element)) {
|
|
17
|
-
return element;
|
|
18
|
-
}
|
|
19
|
-
const focusableDescendant = findDescendant(element, elementIsFocusable);
|
|
20
|
-
return focusableDescendant;
|
|
21
|
-
};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
- https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/Focusgroup/explainer.md
|
|
4
|
-
- https://open-ui.org/components/focusgroup.explainer/
|
|
5
|
-
- https://github.com/openui/open-ui/issues/990
|
|
6
|
-
|
|
7
|
-
- https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/Focusgroup/explainer.md#69-grid-focusgroups
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { performArrowNavigation } from "./arrow_navigation.js";
|
|
11
|
-
import { setFocusGroup } from "./focus_group_registry.js";
|
|
12
|
-
import { isFocusNavMarked } from "./focus_nav_event_marker.js";
|
|
13
|
-
import { performTabNavigation } from "./tab_navigation.js";
|
|
14
|
-
|
|
15
|
-
export const initFocusGroup = (
|
|
16
|
-
element,
|
|
17
|
-
{
|
|
18
|
-
direction = "both",
|
|
19
|
-
// extend = true,
|
|
20
|
-
skipTab = true,
|
|
21
|
-
loop = false,
|
|
22
|
-
name, // Can be undefined for implicit ancestor-descendant grouping
|
|
23
|
-
} = {},
|
|
24
|
-
) => {
|
|
25
|
-
const cleanupCallbackSet = new Set();
|
|
26
|
-
const cleanup = () => {
|
|
27
|
-
for (const callback of cleanupCallbackSet) {
|
|
28
|
-
callback();
|
|
29
|
-
}
|
|
30
|
-
cleanupCallbackSet.clear();
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Store focus group data in registry
|
|
34
|
-
const removeFocusGroup = setFocusGroup(element, {
|
|
35
|
-
direction,
|
|
36
|
-
loop,
|
|
37
|
-
name, // Store undefined as-is for implicit grouping
|
|
38
|
-
});
|
|
39
|
-
cleanupCallbackSet.add(removeFocusGroup);
|
|
40
|
-
|
|
41
|
-
tab: {
|
|
42
|
-
if (!skipTab) {
|
|
43
|
-
break tab;
|
|
44
|
-
}
|
|
45
|
-
const handleTabKeyDown = (event) => {
|
|
46
|
-
if (isFocusNavMarked(event)) {
|
|
47
|
-
// Prevent double handling of the same event + allow preventing focus nav from outside
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
performTabNavigation(event, { outsideOfElement: element });
|
|
51
|
-
};
|
|
52
|
-
// Handle Tab navigation (exit group)
|
|
53
|
-
element.addEventListener("keydown", handleTabKeyDown, {
|
|
54
|
-
// we must use capture: false to let chance for other part of the code
|
|
55
|
-
// to call preventFocusNav
|
|
56
|
-
capture: false,
|
|
57
|
-
passive: false,
|
|
58
|
-
});
|
|
59
|
-
cleanupCallbackSet.add(() => {
|
|
60
|
-
element.removeEventListener("keydown", handleTabKeyDown, {
|
|
61
|
-
capture: false,
|
|
62
|
-
passive: false,
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Handle Arrow key navigation (within group)
|
|
68
|
-
arrow_keys: {
|
|
69
|
-
const handleArrowKeyDown = (event) => {
|
|
70
|
-
if (isFocusNavMarked(event)) {
|
|
71
|
-
// Prevent double handling of the same event + allow preventing focus nav from outside
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
performArrowNavigation(event, element, { direction, loop, name });
|
|
75
|
-
};
|
|
76
|
-
element.addEventListener("keydown", handleArrowKeyDown, {
|
|
77
|
-
// we must use capture: false to let chance for other part of the code
|
|
78
|
-
// to call preventFocusNav
|
|
79
|
-
capture: false,
|
|
80
|
-
passive: false,
|
|
81
|
-
});
|
|
82
|
-
cleanupCallbackSet.add(() => {
|
|
83
|
-
element.removeEventListener("keydown", handleArrowKeyDown, {
|
|
84
|
-
capture: false,
|
|
85
|
-
passive: false,
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { cleanup };
|
|
91
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// WeakMap to store focus group metadata
|
|
2
|
-
export const focusGroupRegistry = new WeakMap();
|
|
3
|
-
|
|
4
|
-
export const setFocusGroup = (element, options) => {
|
|
5
|
-
focusGroupRegistry.set(element, options);
|
|
6
|
-
return () => {
|
|
7
|
-
focusGroupRegistry.delete(element);
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
export const getFocusGroup = (element) => {
|
|
11
|
-
return focusGroupRegistry.get(element);
|
|
12
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { preventFocusNav } from "./focus_nav_event_marker.js";
|
|
2
|
-
|
|
3
|
-
export const preventFocusNavViaKeyboard = (keyboardEvent) => {
|
|
4
|
-
if (keyboardEvent.key === "Tab") {
|
|
5
|
-
// prevent tab to move focus
|
|
6
|
-
keyboardEvent.preventDefault();
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
// ensure we won't perform our internal focus nav in focus groups
|
|
10
|
-
preventFocusNav(keyboardEvent);
|
|
11
|
-
return false;
|
|
12
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { createEventMarker } from "../event_marker.js";
|
|
2
|
-
|
|
3
|
-
export const focusNavEventMarker = createEventMarker("focus_nav");
|
|
4
|
-
|
|
5
|
-
export const preventFocusNav = (event) => {
|
|
6
|
-
focusNavEventMarker.mark(event);
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const isFocusNavMarked = (event) => {
|
|
10
|
-
return focusNavEventMarker.isMarked(event);
|
|
11
|
-
};
|
|
12
|
-
export const markFocusNav = (event) => {
|
|
13
|
-
focusNavEventMarker.mark(event);
|
|
14
|
-
};
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { performTabNavigation } from "./tab_navigation.js";
|
|
2
|
-
|
|
3
|
-
export const trapFocusInside = (element) => {
|
|
4
|
-
if (element.nodeType === 3) {
|
|
5
|
-
console.warn("cannot trap focus inside a text node");
|
|
6
|
-
return () => {};
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const trappedElement = activeTraps.find(
|
|
10
|
-
(activeTrap) => activeTrap.element === element,
|
|
11
|
-
);
|
|
12
|
-
if (trappedElement) {
|
|
13
|
-
console.warn("focus already trapped inside this element");
|
|
14
|
-
return () => {};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const isEventOutside = (event) => {
|
|
18
|
-
if (event.target === element) return false;
|
|
19
|
-
if (element.contains(event.target)) return false;
|
|
20
|
-
return true;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const lock = () => {
|
|
24
|
-
const onmousedown = (event) => {
|
|
25
|
-
if (isEventOutside(event)) {
|
|
26
|
-
event.preventDefault();
|
|
27
|
-
event.stopImmediatePropagation();
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const onkeydown = (event) => {
|
|
32
|
-
if (isTabEvent(event)) {
|
|
33
|
-
performTabNavigation(event, { rootElement: element });
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
document.addEventListener("mousedown", onmousedown, {
|
|
38
|
-
capture: true,
|
|
39
|
-
passive: false,
|
|
40
|
-
});
|
|
41
|
-
document.addEventListener("keydown", onkeydown, {
|
|
42
|
-
capture: true,
|
|
43
|
-
passive: false,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
document.removeEventListener("mousedown", onmousedown, {
|
|
48
|
-
capture: true,
|
|
49
|
-
passive: false,
|
|
50
|
-
});
|
|
51
|
-
document.removeEventListener("keydown", onkeydown, {
|
|
52
|
-
capture: true,
|
|
53
|
-
passive: false,
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const deactivate = activate({
|
|
59
|
-
// element
|
|
60
|
-
lock,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const untrap = () => {
|
|
64
|
-
deactivate();
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return untrap;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const isTabEvent = (event) => event.key === "Tab" || event.keyCode === 9;
|
|
71
|
-
|
|
72
|
-
const activeTraps = [];
|
|
73
|
-
const activate = ({ lock }) => {
|
|
74
|
-
// unlock any trap currently activated
|
|
75
|
-
let previousTrap;
|
|
76
|
-
if (activeTraps.length > 0) {
|
|
77
|
-
previousTrap = activeTraps[activeTraps.length - 1];
|
|
78
|
-
previousTrap.unlock();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// store trap methods to lock/unlock as traps are acivated/deactivated
|
|
82
|
-
const trap = { lock, unlock: lock() };
|
|
83
|
-
activeTraps.push(trap);
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
if (activeTraps.length === 0) {
|
|
87
|
-
console.warn("cannot deactivate an already deactivated trap");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const lastTrap = activeTraps[activeTraps.length - 1];
|
|
91
|
-
if (trap !== lastTrap) {
|
|
92
|
-
// TODO: investigate this and maybe remove this requirment
|
|
93
|
-
console.warn(
|
|
94
|
-
"you must deactivate trap in the same order they were activated",
|
|
95
|
-
);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
activeTraps.pop();
|
|
99
|
-
trap.unlock();
|
|
100
|
-
// if any,reactivate the previous trap
|
|
101
|
-
if (previousTrap) {
|
|
102
|
-
previousTrap.unlock = previousTrap.lock();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
};
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findAfter,
|
|
3
|
-
findBefore,
|
|
4
|
-
findDescendant,
|
|
5
|
-
findLastDescendant,
|
|
6
|
-
} from "../../traversal.js";
|
|
7
|
-
import { elementIsFocusable } from "./element_is_focusable.js";
|
|
8
|
-
import { markFocusNav } from "./focus_nav_event_marker.js";
|
|
9
|
-
|
|
10
|
-
const DEBUG = true;
|
|
11
|
-
|
|
12
|
-
export const performTabNavigation = (
|
|
13
|
-
event,
|
|
14
|
-
{ rootElement = document.body, outsideOfElement = null } = {},
|
|
15
|
-
) => {
|
|
16
|
-
if (!isTabEvent(event)) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const activeElement = document.activeElement;
|
|
20
|
-
if (activeElement.getAttribute("data-focusnav") === "none") {
|
|
21
|
-
event.preventDefault(); // ensure tab cannot move focus
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
const isForward = !event.shiftKey;
|
|
25
|
-
const onTargetToFocus = (targetToFocus) => {
|
|
26
|
-
console.debug(
|
|
27
|
-
`Tab navigation: ${isForward ? "forward" : "backward"} from`,
|
|
28
|
-
activeElement,
|
|
29
|
-
"to",
|
|
30
|
-
targetToFocus,
|
|
31
|
-
);
|
|
32
|
-
event.preventDefault();
|
|
33
|
-
markFocusNav(event);
|
|
34
|
-
targetToFocus.focus();
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (DEBUG) {
|
|
38
|
-
console.debug(
|
|
39
|
-
`Tab navigation: ${isForward ? "forward" : "backward"} from,`,
|
|
40
|
-
activeElement,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const predicate = (candidate) => {
|
|
45
|
-
const canBeFocusedByTab = isFocusableByTab(candidate);
|
|
46
|
-
if (DEBUG) {
|
|
47
|
-
console.debug(`Testing`, candidate, `${canBeFocusedByTab ? "✓" : "✗"}`);
|
|
48
|
-
}
|
|
49
|
-
return canBeFocusedByTab;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const activeElementIsRoot = activeElement === rootElement;
|
|
53
|
-
forward: {
|
|
54
|
-
if (!isForward) {
|
|
55
|
-
break forward;
|
|
56
|
-
}
|
|
57
|
-
if (activeElementIsRoot) {
|
|
58
|
-
const firstFocusableElement = findDescendant(activeElement, predicate, {
|
|
59
|
-
skipRoot: outsideOfElement,
|
|
60
|
-
});
|
|
61
|
-
if (firstFocusableElement) {
|
|
62
|
-
return onTargetToFocus(firstFocusableElement);
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
const nextFocusableElement = findAfter(activeElement, predicate, {
|
|
67
|
-
root: rootElement,
|
|
68
|
-
skipRoot: outsideOfElement,
|
|
69
|
-
});
|
|
70
|
-
if (nextFocusableElement) {
|
|
71
|
-
return onTargetToFocus(nextFocusableElement);
|
|
72
|
-
}
|
|
73
|
-
const firstFocusableElement = findDescendant(activeElement, predicate, {
|
|
74
|
-
skipRoot: outsideOfElement,
|
|
75
|
-
});
|
|
76
|
-
if (firstFocusableElement) {
|
|
77
|
-
return onTargetToFocus(firstFocusableElement);
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
backward: {
|
|
83
|
-
if (activeElementIsRoot) {
|
|
84
|
-
const lastFocusableElement = findLastDescendant(
|
|
85
|
-
activeElement,
|
|
86
|
-
predicate,
|
|
87
|
-
{
|
|
88
|
-
skipRoot: outsideOfElement,
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
if (lastFocusableElement) {
|
|
92
|
-
return onTargetToFocus(lastFocusableElement);
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const previousFocusableElement = findBefore(activeElement, predicate, {
|
|
98
|
-
root: rootElement,
|
|
99
|
-
skipRoot: outsideOfElement,
|
|
100
|
-
});
|
|
101
|
-
if (previousFocusableElement) {
|
|
102
|
-
return onTargetToFocus(previousFocusableElement);
|
|
103
|
-
}
|
|
104
|
-
const lastFocusableElement = findLastDescendant(activeElement, predicate, {
|
|
105
|
-
skipRoot: outsideOfElement,
|
|
106
|
-
});
|
|
107
|
-
if (lastFocusableElement) {
|
|
108
|
-
return onTargetToFocus(lastFocusableElement);
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export const isTabEvent = (event) => event.key === "Tab" || event.keyCode === 9;
|
|
115
|
-
|
|
116
|
-
const isFocusableByTab = (element) => {
|
|
117
|
-
if (hasNegativeTabIndex(element)) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
return elementIsFocusable(element);
|
|
121
|
-
};
|
|
122
|
-
const hasNegativeTabIndex = (element) => {
|
|
123
|
-
return (
|
|
124
|
-
element.hasAttribute &&
|
|
125
|
-
element.hasAttribute("tabIndex") &&
|
|
126
|
-
Number(element.getAttribute("tabindex")) < 0
|
|
127
|
-
);
|
|
128
|
-
};
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Focus Group Test - Skip Tab</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family:
|
|
10
|
-
system-ui,
|
|
11
|
-
-apple-system,
|
|
12
|
-
sans-serif;
|
|
13
|
-
margin: 40px;
|
|
14
|
-
line-height: 1.6;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.test-section {
|
|
18
|
-
margin: 30px 0;
|
|
19
|
-
padding: 20px;
|
|
20
|
-
border: 2px solid #e0e0e0;
|
|
21
|
-
border-radius: 8px;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.focus-group {
|
|
25
|
-
background: #f5f5f5;
|
|
26
|
-
border: 2px dashed #999;
|
|
27
|
-
padding: 15px;
|
|
28
|
-
margin: 10px 0;
|
|
29
|
-
border-radius: 4px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.focus-group h3 {
|
|
33
|
-
margin-top: 0;
|
|
34
|
-
color: #666;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
button,
|
|
38
|
-
input,
|
|
39
|
-
select,
|
|
40
|
-
textarea {
|
|
41
|
-
margin: 5px;
|
|
42
|
-
padding: 8px 12px;
|
|
43
|
-
border: 1px solid #ccc;
|
|
44
|
-
border-radius: 4px;
|
|
45
|
-
font-size: 14px;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
button:focus,
|
|
49
|
-
input:focus,
|
|
50
|
-
select:focus,
|
|
51
|
-
textarea:focus {
|
|
52
|
-
outline: 2px solid #007acc;
|
|
53
|
-
outline-offset: 2px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.instructions {
|
|
57
|
-
background: #e8f4fd;
|
|
58
|
-
padding: 15px;
|
|
59
|
-
border-radius: 4px;
|
|
60
|
-
margin-bottom: 20px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.status {
|
|
64
|
-
position: fixed;
|
|
65
|
-
top: 10px;
|
|
66
|
-
right: 10px;
|
|
67
|
-
background: #333;
|
|
68
|
-
color: white;
|
|
69
|
-
padding: 10px;
|
|
70
|
-
border-radius: 4px;
|
|
71
|
-
font-family: monospace;
|
|
72
|
-
font-size: 12px;
|
|
73
|
-
}
|
|
74
|
-
</style>
|
|
75
|
-
</head>
|
|
76
|
-
<body>
|
|
77
|
-
<div class="status" id="status">Focus: none</div>
|
|
78
|
-
|
|
79
|
-
<h1>Focus Group Test - Skip Tab</h1>
|
|
80
|
-
|
|
81
|
-
<div class="instructions">
|
|
82
|
-
<strong>Test Instructions:</strong>
|
|
83
|
-
<ol>
|
|
84
|
-
<li>Use <kbd>Tab</kbd> to navigate through focusable elements</li>
|
|
85
|
-
<li>
|
|
86
|
-
Elements in the focus group are still accessible (click them or
|
|
87
|
-
navigate normally)
|
|
88
|
-
</li>
|
|
89
|
-
<li>
|
|
90
|
-
When you're focused on an element INSIDE the focus group and press
|
|
91
|
-
<kbd>Tab</kbd>, it should jump to the next focusable element OUTSIDE
|
|
92
|
-
the group
|
|
93
|
-
</li>
|
|
94
|
-
<li>
|
|
95
|
-
Elements before and after the focus group should have normal tab
|
|
96
|
-
behavior
|
|
97
|
-
</li>
|
|
98
|
-
<li>
|
|
99
|
-
Try clicking on elements inside the focus group, then pressing
|
|
100
|
-
<kbd>Tab</kbd>
|
|
101
|
-
</li>
|
|
102
|
-
</ol>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
<div class="test-section">
|
|
106
|
-
<h2>Before Focus Group</h2>
|
|
107
|
-
<button>Button Before 1</button>
|
|
108
|
-
<input type="text" placeholder="Input Before 1" />
|
|
109
|
-
<button>Button Before 2</button>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div class="test-section">
|
|
113
|
-
<h2>Focus Group (skipTab = true)</h2>
|
|
114
|
-
<div class="focus-group" id="focus-group-1">
|
|
115
|
-
<h3>� Tab from inside this group jumps outside</h3>
|
|
116
|
-
<button>Button in Group 1</button>
|
|
117
|
-
<input type="text" placeholder="Input in Group 1" />
|
|
118
|
-
<select>
|
|
119
|
-
<option>Option 1</option>
|
|
120
|
-
<option>Option 2</option>
|
|
121
|
-
</select>
|
|
122
|
-
<button>Button in Group 2</button>
|
|
123
|
-
<textarea placeholder="Textarea in Group"></textarea>
|
|
124
|
-
<button>Button in Group 3</button>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<div class="test-section">
|
|
129
|
-
<h2>After Focus Group</h2>
|
|
130
|
-
<button>Button After 1</button>
|
|
131
|
-
<input type="text" placeholder="Input After 1" />
|
|
132
|
-
<button>Button After 2</button>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<div class="test-section">
|
|
136
|
-
<h2>Control Group (no focus group)</h2>
|
|
137
|
-
<div class="focus-group" style="border-color: #4caf50">
|
|
138
|
-
<h3>✅ This group has normal tab behavior</h3>
|
|
139
|
-
<button>Normal Button 1</button>
|
|
140
|
-
<input type="text" placeholder="Normal Input 1" />
|
|
141
|
-
<button>Normal Button 2</button>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<script type="module">
|
|
146
|
-
import { initFocusGroup } from "../focus_group.js";
|
|
147
|
-
|
|
148
|
-
// Initialize the focus group with skipTab enabled
|
|
149
|
-
const focusGroup = document.getElementById("focus-group-1");
|
|
150
|
-
initFocusGroup(focusGroup, {
|
|
151
|
-
skipTab: true,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Status tracking for debugging
|
|
155
|
-
const statusEl = document.getElementById("status");
|
|
156
|
-
|
|
157
|
-
function updateStatus() {
|
|
158
|
-
const activeElement = document.activeElement;
|
|
159
|
-
const tagName = activeElement.tagName.toLowerCase();
|
|
160
|
-
const text =
|
|
161
|
-
activeElement.textContent ||
|
|
162
|
-
activeElement.placeholder ||
|
|
163
|
-
activeElement.value ||
|
|
164
|
-
"";
|
|
165
|
-
const truncatedText =
|
|
166
|
-
text.length > 20 ? `${text.substring(0, 20)}...` : text;
|
|
167
|
-
|
|
168
|
-
statusEl.textContent = `Focus: ${tagName}${truncatedText ? ` (${truncatedText})` : ""}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Track focus changes
|
|
172
|
-
document.addEventListener("focusin", updateStatus);
|
|
173
|
-
document.addEventListener("focusout", updateStatus);
|
|
174
|
-
|
|
175
|
-
// Initial status
|
|
176
|
-
updateStatus();
|
|
177
|
-
|
|
178
|
-
// Add some debugging
|
|
179
|
-
focusGroup.addEventListener(
|
|
180
|
-
"keydown",
|
|
181
|
-
(e) => {
|
|
182
|
-
if (e.key === "Tab") {
|
|
183
|
-
console.log("Tab pressed in focus group", {
|
|
184
|
-
target: e.target,
|
|
185
|
-
shiftKey: e.shiftKey,
|
|
186
|
-
defaultPrevented: e.defaultPrevented,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
{ capture: true },
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
// Log tab events globally for debugging
|
|
194
|
-
document.addEventListener("keydown", (e) => {
|
|
195
|
-
if (e.key === "Tab") {
|
|
196
|
-
console.log("Global tab event:", {
|
|
197
|
-
target: e.target,
|
|
198
|
-
shiftKey: e.shiftKey,
|
|
199
|
-
defaultPrevented: e.defaultPrevented,
|
|
200
|
-
activeElement: document.activeElement,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
</script>
|
|
205
|
-
</body>
|
|
206
|
-
</html>
|