@jsenv/dom 0.16.0 → 0.17.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 +97 -33
- package/package.json +3 -3
package/dist/jsenv_dom.js
CHANGED
|
@@ -5550,7 +5550,22 @@ const performTabNavigation = (
|
|
|
5550
5550
|
if (hasNegativeTabIndex(element)) {
|
|
5551
5551
|
return false;
|
|
5552
5552
|
}
|
|
5553
|
-
|
|
5553
|
+
if (!elementIsFocusable(element, { excludeAriaHidden })) {
|
|
5554
|
+
return false;
|
|
5555
|
+
}
|
|
5556
|
+
// Native radio-group semantics: within a named radio group only ONE radio
|
|
5557
|
+
// is a Tab stop — the checked one, or the first focusable one when none is
|
|
5558
|
+
// checked. The rest are reachable with arrow keys, not Tab. Without this,
|
|
5559
|
+
// tabbing into a group would land on its first radio instead of its checked
|
|
5560
|
+
// value (e.g. tabbing between two wheels of a WheelGroup).
|
|
5561
|
+
if (
|
|
5562
|
+
element.matches?.('input[type="radio"]') &&
|
|
5563
|
+
element.name &&
|
|
5564
|
+
!radioIsGroupTabStop(element)
|
|
5565
|
+
) {
|
|
5566
|
+
return false;
|
|
5567
|
+
}
|
|
5568
|
+
return true;
|
|
5554
5569
|
};
|
|
5555
5570
|
|
|
5556
5571
|
// A focus group "owns" the activeElement when activeElement is inside it.
|
|
@@ -5701,6 +5716,43 @@ const performTabNavigation = (
|
|
|
5701
5716
|
}
|
|
5702
5717
|
};
|
|
5703
5718
|
|
|
5719
|
+
// Whether a radio is the single Tab stop of its native radio group (checked, or
|
|
5720
|
+
// the first enabled radio when none is checked). Mirrors how the browser puts
|
|
5721
|
+
// only one radio of a group in the Tab order.
|
|
5722
|
+
const radioIsGroupTabStop = (radio) => {
|
|
5723
|
+
const scope = radio.form || radio.getRootNode();
|
|
5724
|
+
if (!scope || !scope.querySelectorAll) {
|
|
5725
|
+
return true;
|
|
5726
|
+
}
|
|
5727
|
+
const sameName = scope.querySelectorAll(
|
|
5728
|
+
`input[type="radio"][name="${CSS.escape(radio.name)}"]`,
|
|
5729
|
+
);
|
|
5730
|
+
const radioForm = radio.form || null;
|
|
5731
|
+
let checked = null;
|
|
5732
|
+
let firstEnabled = null;
|
|
5733
|
+
let groupSize = 0;
|
|
5734
|
+
for (const candidate of sameName) {
|
|
5735
|
+
// Radios only form one group when they share the same form owner.
|
|
5736
|
+
if ((candidate.form || null) !== radioForm) {
|
|
5737
|
+
continue;
|
|
5738
|
+
}
|
|
5739
|
+
groupSize++;
|
|
5740
|
+
if (candidate.disabled) {
|
|
5741
|
+
continue;
|
|
5742
|
+
}
|
|
5743
|
+
if (!firstEnabled) {
|
|
5744
|
+
firstEnabled = candidate;
|
|
5745
|
+
}
|
|
5746
|
+
if (candidate.checked && !checked) {
|
|
5747
|
+
checked = candidate;
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
if (groupSize <= 1) {
|
|
5751
|
+
return true;
|
|
5752
|
+
}
|
|
5753
|
+
return radio === (checked || firstEnabled);
|
|
5754
|
+
};
|
|
5755
|
+
|
|
5704
5756
|
const isTabEvent$1 = (event) => event.key === "Tab" || event.keyCode === 9;
|
|
5705
5757
|
|
|
5706
5758
|
const hasNegativeTabIndex = (element) => {
|
|
@@ -7176,49 +7228,61 @@ const getPaddingSizes = (element) => {
|
|
|
7176
7228
|
*/
|
|
7177
7229
|
const trapScrollInside = (element) => {
|
|
7178
7230
|
const cleanupCallbackSet = new Set();
|
|
7179
|
-
|
|
7231
|
+
|
|
7232
|
+
// Collect every element to lock first (preceding scrollable siblings + all
|
|
7233
|
+
// ancestor scroll containers).
|
|
7234
|
+
const elementsToLock = [];
|
|
7235
|
+
let previous = element.previousSibling;
|
|
7236
|
+
while (previous) {
|
|
7237
|
+
if (previous.nodeType === 1 && isScrollable(previous)) {
|
|
7238
|
+
elementsToLock.push(previous);
|
|
7239
|
+
}
|
|
7240
|
+
previous = previous.previousSibling;
|
|
7241
|
+
}
|
|
7242
|
+
for (const selfOrAncestorScroll of getSelfAndAncestorScrolls(element)) {
|
|
7243
|
+
elementsToLock.push(selfOrAncestorScroll.scrollContainer);
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
// Phase 1 — MEASURE. Batch every layout/style read (scrollTop, scrollbar
|
|
7247
|
+
// size, padding) before any style write, so the layout that showModal
|
|
7248
|
+
// invalidated is recomputed once rather than thrashing between each write and
|
|
7249
|
+
// the next read. (measureScrollbar still forces its own reflow per element via
|
|
7250
|
+
// its probe node — that one is inherent.)
|
|
7251
|
+
const plans = elementsToLock.map((el) => {
|
|
7180
7252
|
const savedScrollTop = el.scrollTop;
|
|
7181
7253
|
const savedScrollLeft = el.scrollLeft;
|
|
7182
7254
|
const scrollbarGutter = getStyle(el, "scrollbar-gutter");
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
el.scrollLeft = savedScrollLeft;
|
|
7192
|
-
});
|
|
7193
|
-
return;
|
|
7255
|
+
if (scrollbarGutter && scrollbarGutter !== "auto") {
|
|
7256
|
+
// The element manages its own gutter — just hide overflow, no padding.
|
|
7257
|
+
return {
|
|
7258
|
+
el,
|
|
7259
|
+
savedScrollTop,
|
|
7260
|
+
savedScrollLeft,
|
|
7261
|
+
styles: { overflow: "hidden" },
|
|
7262
|
+
};
|
|
7194
7263
|
}
|
|
7195
7264
|
const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
|
|
7196
7265
|
const { right, bottom } = getPaddingSizes(el);
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7266
|
+
return {
|
|
7267
|
+
el,
|
|
7268
|
+
savedScrollTop,
|
|
7269
|
+
savedScrollLeft,
|
|
7270
|
+
styles: {
|
|
7271
|
+
"padding-right": `${right + scrollbarWidth}px`,
|
|
7272
|
+
"padding-bottom": `${bottom + scrollbarHeight}px`,
|
|
7273
|
+
"overflow": "hidden",
|
|
7274
|
+
},
|
|
7275
|
+
};
|
|
7276
|
+
});
|
|
7277
|
+
|
|
7278
|
+
// Phase 2 — MUTATE. All style writes together.
|
|
7279
|
+
for (const { el, savedScrollTop, savedScrollLeft, styles } of plans) {
|
|
7280
|
+
const removeScrollLockStyles = setStyles(el, styles);
|
|
7202
7281
|
cleanupCallbackSet.add(() => {
|
|
7203
7282
|
removeScrollLockStyles();
|
|
7204
7283
|
el.scrollTop = savedScrollTop;
|
|
7205
7284
|
el.scrollLeft = savedScrollLeft;
|
|
7206
7285
|
});
|
|
7207
|
-
};
|
|
7208
|
-
let previous = element.previousSibling;
|
|
7209
|
-
while (previous) {
|
|
7210
|
-
if (previous.nodeType === 1) {
|
|
7211
|
-
if (isScrollable(previous)) {
|
|
7212
|
-
lockScroll(previous);
|
|
7213
|
-
}
|
|
7214
|
-
}
|
|
7215
|
-
previous = previous.previousSibling;
|
|
7216
|
-
}
|
|
7217
|
-
|
|
7218
|
-
const selfAndAncestorScrolls = getSelfAndAncestorScrolls(element);
|
|
7219
|
-
for (const selfOrAncestorScroll of selfAndAncestorScrolls) {
|
|
7220
|
-
const elementToScrollLock = selfOrAncestorScroll.scrollContainer;
|
|
7221
|
-
lockScroll(elementToScrollLock);
|
|
7222
7286
|
}
|
|
7223
7287
|
|
|
7224
7288
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DOM utilities for writing frontend code",
|
|
6
6
|
"repository": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@jsenv/core": "../../../",
|
|
41
41
|
"@jsenv/navi": "../navi",
|
|
42
42
|
"@jsenv/snapshot": "../../tooling/snapshot",
|
|
43
|
-
"@preact/signals": "2.9.
|
|
44
|
-
"preact": "11.0.0-beta.
|
|
43
|
+
"@preact/signals": "2.9.4",
|
|
44
|
+
"preact": "11.0.0-beta.2"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|