@jsenv/dom 0.12.2 → 0.14.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 +104 -9
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -2240,6 +2240,7 @@ const cssSizeUnitSet = new Set([
|
|
|
2240
2240
|
"in",
|
|
2241
2241
|
"pt",
|
|
2242
2242
|
"pc",
|
|
2243
|
+
"cap",
|
|
2243
2244
|
]);
|
|
2244
2245
|
const cssUnitSet = new Set([
|
|
2245
2246
|
...cssSizeUnitSet,
|
|
@@ -4446,6 +4447,10 @@ const normalizeKeyboardKey = (rawKey) => {
|
|
|
4446
4447
|
|
|
4447
4448
|
const getKeyboardEventDefaultAction = (keyboardEvent) => {
|
|
4448
4449
|
const target = keyboardEvent.target;
|
|
4450
|
+
if (keyboardEvent.key === undefined) {
|
|
4451
|
+
// Happens for enter after autocomplete
|
|
4452
|
+
return "activate";
|
|
4453
|
+
}
|
|
4449
4454
|
const key = normalizeKeyboardKey(keyboardEvent.key);
|
|
4450
4455
|
|
|
4451
4456
|
// Nothing special occurs when the target or an ancestor is disabled/inert
|
|
@@ -4487,6 +4492,10 @@ const isTypingIntent = (e) => {
|
|
|
4487
4492
|
if (e.metaKey || e.ctrlKey) {
|
|
4488
4493
|
return false;
|
|
4489
4494
|
}
|
|
4495
|
+
if (!e.key) {
|
|
4496
|
+
// can happen when pressing enter for autocomplete for instance
|
|
4497
|
+
return false;
|
|
4498
|
+
}
|
|
4490
4499
|
const key = normalizeKeyboardKey(e.key);
|
|
4491
4500
|
// Single printable character — the user is typing
|
|
4492
4501
|
if (e.key.length === 1) {
|
|
@@ -5450,10 +5459,40 @@ const performTabNavigation = (
|
|
|
5450
5459
|
// A focus group "owns" the activeElement when activeElement is inside it.
|
|
5451
5460
|
// From the inside, Tab should exit the group (skip its remaining children).
|
|
5452
5461
|
// From the outside, Tab should enter the group normally (first focusable child).
|
|
5462
|
+
//
|
|
5463
|
+
// Smart mode (navi-focus-group="[role=radio]"):
|
|
5464
|
+
// - activeElement directly matches the selector (IS a radio):
|
|
5465
|
+
// Tab skips ALL elements in the group → exits to next focusable outside.
|
|
5466
|
+
// - activeElement is inside a managed element but doesn't match (e.g. an
|
|
5467
|
+
// input inside a custom radio widget): Tab navigates freely within the
|
|
5468
|
+
// group, only skipping elements that directly match the managed selector.
|
|
5469
|
+
//
|
|
5470
|
+
// Strict mode (navi-focus-group with no value, or navi-focus-group-strict):
|
|
5471
|
+
// Tab always exits the group regardless of where focus is inside it.
|
|
5453
5472
|
const activeFocusGroup =
|
|
5454
5473
|
activeElement.closest?.("[navi-focus-group]") || null;
|
|
5455
|
-
const
|
|
5456
|
-
|
|
5474
|
+
const activeFocusGroupManages = activeFocusGroup
|
|
5475
|
+
? activeFocusGroup.getAttribute("navi-focus-group") || null
|
|
5476
|
+
: null;
|
|
5477
|
+
const activeFocusGroupIsStrict = activeFocusGroup
|
|
5478
|
+
? !activeFocusGroupManages ||
|
|
5479
|
+
activeFocusGroup.hasAttribute("navi-focus-group-strict")
|
|
5480
|
+
: false;
|
|
5481
|
+
const activeElementIsManaged =
|
|
5482
|
+
activeFocusGroup && activeFocusGroupManages
|
|
5483
|
+
? activeElement.matches(activeFocusGroupManages)
|
|
5484
|
+
: false;
|
|
5485
|
+
const isOwnedByActiveFocusGroup = (el) => {
|
|
5486
|
+
if (!activeFocusGroup || !activeFocusGroup.contains(el)) {
|
|
5487
|
+
return false;
|
|
5488
|
+
}
|
|
5489
|
+
if (activeFocusGroupIsStrict || activeElementIsManaged) {
|
|
5490
|
+
// Strict: skip everything inside the group so Tab exits.
|
|
5491
|
+
return true;
|
|
5492
|
+
}
|
|
5493
|
+
// Smart: only skip elements that are themselves managed items.
|
|
5494
|
+
return el.matches(activeFocusGroupManages);
|
|
5495
|
+
};
|
|
5457
5496
|
|
|
5458
5497
|
const predicate = (candidate, skip) => {
|
|
5459
5498
|
if (!isFocusableByTab(candidate)) {
|
|
@@ -5588,6 +5627,12 @@ const hasNegativeTabIndex = (element) => {
|
|
|
5588
5627
|
* when navigating on the x axis. Omit to allow any focusable element.
|
|
5589
5628
|
* @param {string} [options.ySelector] - CSS selector that candidates must match
|
|
5590
5629
|
* when navigating on the y axis. Omit to allow any focusable element.
|
|
5630
|
+
* @param {string} [options.manages] - CSS selector declaring which descendants
|
|
5631
|
+
* this group "manages" for Tab navigation. When set, Tab only skips managed
|
|
5632
|
+
* elements; other focusable descendants (e.g. inputs inside a radio widget)
|
|
5633
|
+
* remain individually tabbable. When omitted, Tab skips the entire group.
|
|
5634
|
+
* @param {boolean} [options.strictTab=false] - When true AND manages is set,
|
|
5635
|
+
* Tab always exits the group regardless of where focus is inside it.
|
|
5591
5636
|
* @returns {{ cleanup: () => void }} Call cleanup() to remove all event listeners.
|
|
5592
5637
|
*/
|
|
5593
5638
|
const initFocusGroup = (
|
|
@@ -5604,6 +5649,10 @@ const initFocusGroup = (
|
|
|
5604
5649
|
// CSS selector to restrict candidates on each axis
|
|
5605
5650
|
xSelector,
|
|
5606
5651
|
ySelector,
|
|
5652
|
+
// CSS selector declaring which elements the group "manages" for Tab purposes.
|
|
5653
|
+
// Defaults to ySelector ?? xSelector so arrow-nav and tab-nav stay in sync.
|
|
5654
|
+
manages = ySelector ?? xSelector,
|
|
5655
|
+
strictTab = false,
|
|
5607
5656
|
} = {},
|
|
5608
5657
|
) => {
|
|
5609
5658
|
const cleanupCallbackSet = new Set();
|
|
@@ -5620,10 +5669,16 @@ const initFocusGroup = (
|
|
|
5620
5669
|
name, // Store undefined as-is for implicit grouping
|
|
5621
5670
|
});
|
|
5622
5671
|
cleanupCallbackSet.add(removeFocusGroup);
|
|
5623
|
-
element.setAttribute("navi-focus-group", "");
|
|
5672
|
+
element.setAttribute("navi-focus-group", manages ?? "");
|
|
5624
5673
|
cleanupCallbackSet.add(() => {
|
|
5625
5674
|
element.removeAttribute("navi-focus-group");
|
|
5626
5675
|
});
|
|
5676
|
+
if (manages && strictTab) {
|
|
5677
|
+
element.setAttribute("navi-focus-group-strict", "");
|
|
5678
|
+
cleanupCallbackSet.add(() => {
|
|
5679
|
+
element.removeAttribute("navi-focus-group-strict");
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5627
5682
|
|
|
5628
5683
|
tab: {
|
|
5629
5684
|
if (!skipTab) {
|
|
@@ -5634,8 +5689,18 @@ const initFocusGroup = (
|
|
|
5634
5689
|
// Prevent double handling of the same event + allow preventing focus nav from outside
|
|
5635
5690
|
return;
|
|
5636
5691
|
}
|
|
5692
|
+
// Smart mode: when focus is inside an unmanaged element (e.g. an input
|
|
5693
|
+
// inside a radio widget), do NOT skip the entire group — let Tab navigate
|
|
5694
|
+
// freely. The predicate in performTabNavigation will still skip managed
|
|
5695
|
+
// items (those matching `manages`) encountered along the way.
|
|
5696
|
+
const activeElement = document.activeElement;
|
|
5697
|
+
const focusIsOnUnmanagedDescendant =
|
|
5698
|
+
manages &&
|
|
5699
|
+
!strictTab &&
|
|
5700
|
+
element.contains(activeElement) &&
|
|
5701
|
+
!activeElement.matches(manages);
|
|
5637
5702
|
performTabNavigation(event, {
|
|
5638
|
-
outsideOfElement: element,
|
|
5703
|
+
outsideOfElement: focusIsOnUnmanagedDescendant ? null : element,
|
|
5639
5704
|
excludeAriaHidden,
|
|
5640
5705
|
});
|
|
5641
5706
|
};
|
|
@@ -11190,13 +11255,43 @@ const visibleRectEffect = (
|
|
|
11190
11255
|
}
|
|
11191
11256
|
}
|
|
11192
11257
|
{
|
|
11193
|
-
|
|
11258
|
+
// visualViewport resize is a superset of window resize:
|
|
11259
|
+
// it also fires when the virtual keyboard opens/closes on mobile.
|
|
11260
|
+
// Fall back to window resize when visualViewport is unavailable.
|
|
11261
|
+
const onResize = (e) => {
|
|
11194
11262
|
autoCheck(e);
|
|
11195
11263
|
};
|
|
11196
|
-
window.
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11264
|
+
if (window.visualViewport) {
|
|
11265
|
+
window.visualViewport.addEventListener("resize", onResize);
|
|
11266
|
+
addTeardown(() => {
|
|
11267
|
+
window.visualViewport.removeEventListener("resize", onResize);
|
|
11268
|
+
});
|
|
11269
|
+
} else {
|
|
11270
|
+
window.addEventListener("resize", onResize);
|
|
11271
|
+
addTeardown(() => {
|
|
11272
|
+
window.removeEventListener("resize", onResize);
|
|
11273
|
+
});
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
{
|
|
11277
|
+
// visualViewport scroll fires when the visual viewport pans independently
|
|
11278
|
+
// of the layout viewport (e.g. during pinch-zoom). This is distinct from
|
|
11279
|
+
// document scroll and must be observed separately.
|
|
11280
|
+
if (window.visualViewport) {
|
|
11281
|
+
const onVisualViewportScroll = (e) => {
|
|
11282
|
+
autoCheck(e);
|
|
11283
|
+
};
|
|
11284
|
+
window.visualViewport.addEventListener(
|
|
11285
|
+
"scroll",
|
|
11286
|
+
onVisualViewportScroll,
|
|
11287
|
+
);
|
|
11288
|
+
addTeardown(() => {
|
|
11289
|
+
window.visualViewport.removeEventListener(
|
|
11290
|
+
"scroll",
|
|
11291
|
+
onVisualViewportScroll,
|
|
11292
|
+
);
|
|
11293
|
+
});
|
|
11294
|
+
}
|
|
11200
11295
|
}
|
|
11201
11296
|
{
|
|
11202
11297
|
const resizeObserver = new ResizeObserver(() => {
|