@jsenv/dom 0.1.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 (101) hide show
  1. package/dist/jsenv_dom.js +9653 -0
  2. package/index.js +101 -0
  3. package/package.json +47 -0
  4. package/src/attr/add_attribute_effect.js +93 -0
  5. package/src/attr/attributes.js +32 -0
  6. package/src/demos/3_columns_resize_demo.html +84 -0
  7. package/src/demos/3_rows_resize_demo.html +89 -0
  8. package/src/demos/aside_and_main_demo.html +93 -0
  9. package/src/demos/coordinates_demo.html +450 -0
  10. package/src/demos/document_autoscroll_demo.html +517 -0
  11. package/src/demos/drag_gesture_constraints_demo.html +701 -0
  12. package/src/demos/drag_gesture_demo.html +1047 -0
  13. package/src/demos/drag_gesture_element_to_impact_demo.html +445 -0
  14. package/src/demos/drag_reference_element_demo.html +480 -0
  15. package/src/demos/flex_details_set_demo.html +302 -0
  16. package/src/demos/flex_details_set_demo_2.html +315 -0
  17. package/src/demos/visible_rect_demo.html +525 -0
  18. package/src/interaction/drag/constraint_feedback_line.js +92 -0
  19. package/src/interaction/drag/drag_constraint.js +659 -0
  20. package/src/interaction/drag/drag_debug_markers.js +635 -0
  21. package/src/interaction/drag/drag_element_positioner.js +382 -0
  22. package/src/interaction/drag/drag_gesture.js +566 -0
  23. package/src/interaction/drag/drag_resize_demo.html +571 -0
  24. package/src/interaction/drag/drag_to_move.js +301 -0
  25. package/src/interaction/drag/drag_to_resize_gesture.js +68 -0
  26. package/src/interaction/drag/drop_target_detection.js +148 -0
  27. package/src/interaction/drag/sticky_frontiers.js +160 -0
  28. package/src/interaction/element_log.js +8 -0
  29. package/src/interaction/event_marker.js +14 -0
  30. package/src/interaction/focus/active_element.js +33 -0
  31. package/src/interaction/focus/arrow_navigation.js +599 -0
  32. package/src/interaction/focus/element_is_focusable.js +57 -0
  33. package/src/interaction/focus/element_is_visible.js +36 -0
  34. package/src/interaction/focus/find_focusable.js +21 -0
  35. package/src/interaction/focus/focus_group.js +91 -0
  36. package/src/interaction/focus/focus_group_registry.js +12 -0
  37. package/src/interaction/focus/focus_nav.js +12 -0
  38. package/src/interaction/focus/focus_nav_event_marker.js +14 -0
  39. package/src/interaction/focus/focus_trap.js +105 -0
  40. package/src/interaction/focus/tab_navigation.js +128 -0
  41. package/src/interaction/focus/tests/focus_group_skip_tab_test.html +206 -0
  42. package/src/interaction/focus/tests/tree_focus_test.html +304 -0
  43. package/src/interaction/focus/tests/tree_focus_test.jsx +261 -0
  44. package/src/interaction/focus/tests/tree_focus_test_preact.html +13 -0
  45. package/src/interaction/isolate_interactions.js +161 -0
  46. package/src/interaction/keyboard.js +26 -0
  47. package/src/interaction/scroll/capture_scroll.js +47 -0
  48. package/src/interaction/scroll/is_scrollable.js +159 -0
  49. package/src/interaction/scroll/scroll_container.js +110 -0
  50. package/src/interaction/scroll/scroll_trap.js +44 -0
  51. package/src/interaction/scroll/scrollbar_size.js +20 -0
  52. package/src/interaction/scroll/wheel_through.js +138 -0
  53. package/src/iterable_weak_set.js +66 -0
  54. package/src/position/dom_coords.js +340 -0
  55. package/src/position/offset_parent.js +15 -0
  56. package/src/position/position_fixed.js +15 -0
  57. package/src/position/position_sticky.js +213 -0
  58. package/src/position/sticky_rect.js +79 -0
  59. package/src/position/visible_rect.js +482 -0
  60. package/src/pub_sub.js +28 -0
  61. package/src/size/can_take_size.js +11 -0
  62. package/src/size/details_content_full_height.js +63 -0
  63. package/src/size/flex_details_set.js +974 -0
  64. package/src/size/get_available_height.js +22 -0
  65. package/src/size/get_available_width.js +22 -0
  66. package/src/size/get_border_sizes.js +14 -0
  67. package/src/size/get_height.js +4 -0
  68. package/src/size/get_inner_height.js +15 -0
  69. package/src/size/get_inner_width.js +15 -0
  70. package/src/size/get_margin_sizes.js +10 -0
  71. package/src/size/get_max_height.js +57 -0
  72. package/src/size/get_max_width.js +47 -0
  73. package/src/size/get_min_height.js +14 -0
  74. package/src/size/get_min_width.js +14 -0
  75. package/src/size/get_padding_sizes.js +10 -0
  76. package/src/size/get_width.js +4 -0
  77. package/src/size/hooks/use_available_height.js +27 -0
  78. package/src/size/hooks/use_available_width.js +27 -0
  79. package/src/size/hooks/use_max_height.js +10 -0
  80. package/src/size/hooks/use_max_width.js +10 -0
  81. package/src/size/hooks/use_resize_status.js +62 -0
  82. package/src/size/resize.js +695 -0
  83. package/src/size/resolve_css_size.js +32 -0
  84. package/src/style/dom_styles.js +97 -0
  85. package/src/style/style_composition.js +78 -0
  86. package/src/style/style_controller.js +345 -0
  87. package/src/style/style_parsing.js +317 -0
  88. package/src/transition/demos/animation_resumption_test.xhtml +500 -0
  89. package/src/transition/demos/height_toggle_test.xhtml +515 -0
  90. package/src/transition/dom_transition.js +254 -0
  91. package/src/transition/easing.js +48 -0
  92. package/src/transition/group_transition.js +261 -0
  93. package/src/transition/transform_style_parser.js +32 -0
  94. package/src/transition/transition_playback.js +366 -0
  95. package/src/transition/transition_timeline.js +79 -0
  96. package/src/traversal.js +247 -0
  97. package/src/ui_transition/demos/content_states_transition_demo.html +628 -0
  98. package/src/ui_transition/demos/smooth_height_transition_demo.html +149 -0
  99. package/src/ui_transition/demos/transition_testing.html +354 -0
  100. package/src/ui_transition/ui_transition.js +1492 -0
  101. package/src/utils.js +69 -0
@@ -0,0 +1,36 @@
1
+ import { getStyle } from "../../style/dom_styles.js";
2
+ import {
3
+ elementIsDetails,
4
+ elementIsSummary,
5
+ isDocumentElement,
6
+ } from "../../utils.js";
7
+
8
+ export const elementIsVisible = (node) => {
9
+ if (isDocumentElement(node)) {
10
+ return true;
11
+ }
12
+ if (getStyle(node, "visibility") === "hidden") {
13
+ return false;
14
+ }
15
+ let nodeOrAncestor = node;
16
+ while (nodeOrAncestor) {
17
+ if (isDocumentElement(nodeOrAncestor)) {
18
+ break;
19
+ }
20
+ if (getStyle(nodeOrAncestor, "display") === "none") {
21
+ return false;
22
+ }
23
+ // Check if element is inside a closed details element
24
+ if (elementIsDetails(nodeOrAncestor) && !nodeOrAncestor.open) {
25
+ // Special case: summary elements are visible even when their parent details is closed
26
+ // But only if this details element is the direct parent of the summary
27
+ if (elementIsSummary(node) && node.parentElement === nodeOrAncestor) {
28
+ // Continue checking ancestors, don't return false yet
29
+ } else {
30
+ return false;
31
+ }
32
+ }
33
+ nodeOrAncestor = nodeOrAncestor.parentNode;
34
+ }
35
+ return true;
36
+ };
@@ -0,0 +1,21 @@
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
+ };
@@ -0,0 +1,91 @@
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
+ };
@@ -0,0 +1,12 @@
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
+ };
@@ -0,0 +1,12 @@
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
+ };
@@ -0,0 +1,14 @@
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
+ };
@@ -0,0 +1,105 @@
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
+ };
@@ -0,0 +1,128 @@
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
+ };
@@ -0,0 +1,206 @@
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>