@jsenv/dom 0.12.0 → 0.12.2
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 +60 -10
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -4375,21 +4375,30 @@ const findFocusDelegateTarget = (el) => {
|
|
|
4375
4375
|
return null;
|
|
4376
4376
|
};
|
|
4377
4377
|
|
|
4378
|
-
const findFocusable = (element) => {
|
|
4378
|
+
const findFocusable = (element, { exclude } = {}) => {
|
|
4379
4379
|
const associatedElements = getAssociatedElements(element);
|
|
4380
4380
|
if (associatedElements) {
|
|
4381
4381
|
for (const associatedElement of associatedElements) {
|
|
4382
|
-
const focusable = findFocusable(associatedElement);
|
|
4382
|
+
const focusable = findFocusable(associatedElement, { exclude });
|
|
4383
4383
|
if (focusable) {
|
|
4384
4384
|
return focusable;
|
|
4385
4385
|
}
|
|
4386
4386
|
}
|
|
4387
4387
|
return null;
|
|
4388
4388
|
}
|
|
4389
|
-
|
|
4389
|
+
const isFocusable = (node) => {
|
|
4390
|
+
if (!elementIsFocusable(node)) {
|
|
4391
|
+
return false;
|
|
4392
|
+
}
|
|
4393
|
+
if (exclude && exclude(node)) {
|
|
4394
|
+
return false;
|
|
4395
|
+
}
|
|
4396
|
+
return true;
|
|
4397
|
+
};
|
|
4398
|
+
if (isFocusable(element)) {
|
|
4390
4399
|
return element;
|
|
4391
4400
|
}
|
|
4392
|
-
const focusableDescendant = findDescendant(element,
|
|
4401
|
+
const focusableDescendant = findDescendant(element, isFocusable);
|
|
4393
4402
|
if (focusableDescendant) {
|
|
4394
4403
|
// If the first focusable is an unchecked radio/checkbox, prefer the checked
|
|
4395
4404
|
// sibling in the same group (mirrors native browser radio focus behavior
|
|
@@ -4496,10 +4505,15 @@ const DEFAULT_BEHAVIORS = [
|
|
|
4496
4505
|
keys: {
|
|
4497
4506
|
// Tab moves focus on any element
|
|
4498
4507
|
tab: "focus_nav",
|
|
4499
|
-
|
|
4508
|
+
},
|
|
4509
|
+
// no fallback: only claims Tab, other keys continue to next entries
|
|
4510
|
+
},
|
|
4511
|
+
{
|
|
4512
|
+
// Escape natively dismisses only <dialog> elements
|
|
4513
|
+
test: (el) => el.tagName === "DIALOG" || Boolean(el.closest("dialog")),
|
|
4514
|
+
keys: {
|
|
4500
4515
|
escape: "dismiss",
|
|
4501
4516
|
},
|
|
4502
|
-
// no fallback: only claims Tab/Escape, other keys continue to next entries
|
|
4503
4517
|
},
|
|
4504
4518
|
{
|
|
4505
4519
|
test: (el) => el.matches("input[type='radio'], input[type='checkbox']"),
|
|
@@ -4518,6 +4532,12 @@ const DEFAULT_BEHAVIORS = [
|
|
|
4518
4532
|
"input:not([type]), input[type='text'], input[type='search'], input[type='url'], input[type='email'], input[type='password'], input[type='tel']",
|
|
4519
4533
|
),
|
|
4520
4534
|
keys: {
|
|
4535
|
+
escape: (e) => {
|
|
4536
|
+
if (e.target.type === "search") {
|
|
4537
|
+
return e.target.value ? "clear" : "";
|
|
4538
|
+
}
|
|
4539
|
+
return "";
|
|
4540
|
+
},
|
|
4521
4541
|
enter: (e) => (e.target.form ? "form_submit" : ""),
|
|
4522
4542
|
arrowleft: "cursor_move",
|
|
4523
4543
|
arrowright: "cursor_move",
|
|
@@ -5427,10 +5447,36 @@ const performTabNavigation = (
|
|
|
5427
5447
|
return elementIsFocusable(element, { excludeAriaHidden });
|
|
5428
5448
|
};
|
|
5429
5449
|
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5450
|
+
// A focus group "owns" the activeElement when activeElement is inside it.
|
|
5451
|
+
// From the inside, Tab should exit the group (skip its remaining children).
|
|
5452
|
+
// From the outside, Tab should enter the group normally (first focusable child).
|
|
5453
|
+
const activeFocusGroup =
|
|
5454
|
+
activeElement.closest?.("[navi-focus-group]") || null;
|
|
5455
|
+
const isOwnedByActiveFocusGroup = (el) =>
|
|
5456
|
+
activeFocusGroup && activeFocusGroup.contains(el);
|
|
5457
|
+
|
|
5458
|
+
const predicate = (candidate, skip) => {
|
|
5459
|
+
if (!isFocusableByTab(candidate)) {
|
|
5460
|
+
return false;
|
|
5461
|
+
}
|
|
5462
|
+
// Focus group roots are composite widgets.
|
|
5463
|
+
if (candidate.hasAttribute("navi-focus-group")) {
|
|
5464
|
+
if (isFocusableByTab(candidate)) {
|
|
5465
|
+
// Root has tabindex="0": it is the single Tab stop for the group.
|
|
5466
|
+
// Skip its children — arrow keys handle internal navigation.
|
|
5467
|
+
skip?.();
|
|
5468
|
+
return true;
|
|
5469
|
+
}
|
|
5470
|
+
// Root is not focusable by Tab: descend into children to allow Tab entry.
|
|
5471
|
+
return false;
|
|
5472
|
+
}
|
|
5473
|
+
// If candidate is inside the focus group that currently owns focus, skip
|
|
5474
|
+
// it — Tab should exit the group. (Going *into* a different focus group
|
|
5475
|
+
// is allowed: only one focus group at a time has the activeElement.)
|
|
5476
|
+
if (isOwnedByActiveFocusGroup(candidate)) {
|
|
5477
|
+
return false;
|
|
5478
|
+
}
|
|
5479
|
+
return true;
|
|
5434
5480
|
};
|
|
5435
5481
|
|
|
5436
5482
|
const activeElementIsRoot = activeElement === rootElement;
|
|
@@ -5574,6 +5620,10 @@ const initFocusGroup = (
|
|
|
5574
5620
|
name, // Store undefined as-is for implicit grouping
|
|
5575
5621
|
});
|
|
5576
5622
|
cleanupCallbackSet.add(removeFocusGroup);
|
|
5623
|
+
element.setAttribute("navi-focus-group", "");
|
|
5624
|
+
cleanupCallbackSet.add(() => {
|
|
5625
|
+
element.removeAttribute("navi-focus-group");
|
|
5626
|
+
});
|
|
5577
5627
|
|
|
5578
5628
|
tab: {
|
|
5579
5629
|
if (!skipTab) {
|