Skip to content

Commit fe9009c

Browse files
asamuzaKdomeniccodex
authored
Update focused area handling
Respect display and the flat tree when determining focusability Prevent focus on elements with display: none on themselves or their flat-tree ancestors, and on elements excluded from the flat tree. Follow shadow hosts and slot assignments, including closed shadow roots, nested slots, unassigned light DOM, and fallback content replaced by assigned nodes. Use computed styles so CSS can override the hidden attribute. Invalidate cached computed styles when focus changes, before dispatching focus and blur events. This keeps :focus, :focus-visible, :focus-within, and selectors containing them current, including during reentrant focus changes and transitions between documents. Allow computed style resolution for elements without inline-style support, while still applying stylesheet rules. This supports focusability checks through MathML and XML ancestors and fixes existing XML selector tests. Co-authored-by: Domenic Denicola <[email protected]> Co-authored-by: codex <[email protected]>
1 parent a8d28b3 commit fe9009c

9 files changed

Lines changed: 533 additions & 52 deletions

File tree

‎lib/jsdom/living/css/helpers/computed-style.js‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ function prepareComputedStyleDeclaration(elementImpl, styleCache) {
5353

5454
applyStyleSheetRules(elementImpl, declaration);
5555

56-
for (let i = 0; i < style.length; i++) {
57-
handlePropertyForInlineStyle(style.item(i), declaration, style);
56+
// Not every element implements `ElementCSSInlineStyle`.
57+
if (style) {
58+
for (let i = 0; i < style.length; i++) {
59+
handlePropertyForInlineStyle(style.item(i), declaration, style);
60+
}
5861
}
5962

6063
return declaration;

‎lib/jsdom/living/helpers/focusing.js‎

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"use strict";
22
const FocusEvent = require("../../../generated/idl/FocusEvent.js");
33
const idlUtils = require("../../../generated/idl/utils.js");
4+
const { getComputedStyleDeclaration } = require("../css/helpers/computed-style.js");
45
const { isDisabled } = require("./form-controls.js");
5-
const { firstChildWithLocalName } = require("./traversal");
6-
const { createAnEvent } = require("./events");
7-
const { HTML_NS, SVG_NS } = require("./namespaces");
8-
const { isRenderedElement } = require("./svg/render");
9-
const NODE_TYPE = require("../node-type");
6+
const { firstChildWithLocalName } = require("./traversal.js");
7+
const { createAnEvent } = require("./events.js");
8+
const { HTML_NS, SVG_NS } = require("./namespaces.js");
9+
const { flatTreeInclusiveAncestors } = require("./shadow-dom.js");
10+
const { isRenderedElement } = require("./svg/render.js");
11+
const NODE_TYPE = require("../node-type.js");
1012

1113
const focusableFormElements = new Set(["input", "select", "textarea", "button"]);
1214

@@ -19,15 +21,28 @@ exports.isFocusableAreaElement = elImpl => {
1921
// https://html.spec.whatwg.org/multipage/interaction.html#tabindex-value
2022
// However, some suggested elements are not focusable in web browsers, as detailed here:
2123
// https://github.com/whatwg/html/issues/5490
22-
if (elImpl._namespaceURI === HTML_NS) {
23-
if (!elImpl._ownerDocument._defaultView) {
24-
return false;
25-
}
24+
if (elImpl._namespaceURI === HTML_NS && !elImpl._ownerDocument._defaultView) {
25+
return false;
26+
}
27+
if (!elImpl._ownerDocument._defaultView || !elImpl.isConnected) {
28+
return false;
29+
}
30+
31+
const ancestors = flatTreeInclusiveAncestors(elImpl);
32+
if (!ancestors) {
33+
return false;
34+
}
2635

27-
if (!elImpl.isConnected) {
28-
return false;
36+
for (const current of ancestors) {
37+
if (current.nodeType === NODE_TYPE.ELEMENT_NODE) {
38+
const declaration = getComputedStyleDeclaration(current);
39+
if (declaration.getPropertyValue("display") === "none") {
40+
return false;
41+
}
2942
}
43+
}
3044

45+
if (elImpl._namespaceURI === HTML_NS) {
3146
// A tabindex cannot make disabled controls or hidden inputs focusable.
3247
if (focusableFormElements.has(elImpl._localName)) {
3348
if (elImpl._localName === "input" && elImpl.type === "hidden") {
@@ -59,11 +74,10 @@ exports.isFocusableAreaElement = elImpl => {
5974
return true;
6075
}
6176

62-
return false;
63-
6477
// This does not check for a designMode Document as specified in
6578
// https://html.spec.whatwg.org/multipage/interaction.html#editing-host because the designMode
6679
// attribute is not implemented.
80+
return false;
6781
}
6882

6983
if (elImpl._namespaceURI === SVG_NS) {
@@ -74,8 +88,6 @@ exports.isFocusableAreaElement = elImpl => {
7488
if (elImpl._localName === "a" && elImpl.hasAttributeNS(null, "href")) {
7589
return true;
7690
}
77-
78-
return false;
7991
}
8092

8193
return false;
@@ -238,6 +250,8 @@ function focusUpdateSteps(oldChain, newChain, newFocusTarget) {
238250
const document = entry._ownerDocument;
239251
document._lastFocusedElement = null;
240252
document._clearDOMSelector();
253+
// Focus selectors can affect styles throughout the document, including in event listeners below.
254+
document._clearStyleCache();
241255
if (entry.nodeType === NODE_TYPE.DOCUMENT_NODE && document._parentDocument) {
242256
document._parentDocument._lastFocusedElement = null;
243257
document._parentDocument._clearDOMSelector();
@@ -283,6 +297,7 @@ function focusUpdateSteps(oldChain, newChain, newFocusTarget) {
283297
// TODO: Set the navigation API's "focus changed during ongoing navigation" flag.
284298
document._lastFocusedElement = entry;
285299
document._clearDOMSelector();
300+
document._clearStyleCache();
286301
}
287302
// Reflect child-document focus in the parent until navigables have their own focus state.
288303
if (entry.nodeType === NODE_TYPE.DOCUMENT_NODE && document._parentDocument) {

‎lib/jsdom/living/helpers/shadow-dom.js‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,50 @@ function signalSlotChange(slot) {
236236
queueMutationObserverMicrotask();
237237
}
238238

239+
// https://drafts.csswg.org/css-shadow/#flat-tree
240+
// Returns inclusive element ancestors, nearest first, or null if the element
241+
// is absent from its document's flat tree.
242+
function flatTreeInclusiveAncestors(element) {
243+
const root = element._ownerDocument.documentElement;
244+
const ancestors = [];
245+
246+
for (let current = element; current !== null; current = flatTreeParentElement(current)) {
247+
ancestors.push(current);
248+
if (current === root) {
249+
return ancestors;
250+
}
251+
}
252+
253+
return null;
254+
}
255+
256+
function flatTreeParentElement(element) {
257+
const parent = element.parentNode;
258+
259+
if (isShadowRoot(parent)) {
260+
return parent.host;
261+
}
262+
263+
if (parent === null || parent.nodeType !== NODE_TYPE.ELEMENT_NODE) {
264+
return null;
265+
}
266+
267+
if (parent._shadowRoot) {
268+
// Unassigned light children are excluded. `findSlot()` includes closed
269+
// roots, unlike the public `assignedSlot` getter.
270+
return findSlot(element);
271+
}
272+
273+
if (isSlot(parent) &&
274+
isShadowRoot(parent.getRootNode()) &&
275+
parent._assignedNodes.length > 0) {
276+
// Assigned nodes replace the slot's fallback children.
277+
return null;
278+
}
279+
280+
return parent;
281+
}
282+
239283
module.exports = {
240284
isValidHostElementName,
241285

@@ -256,5 +300,8 @@ module.exports = {
256300
findSlot,
257301
findFlattenedSlotables,
258302

259-
signalSlotChange
303+
signalSlotChange,
304+
305+
flatTreeInclusiveAncestors,
306+
flatTreeParentElement
260307
};

‎test/web-platform-tests/to-run.yaml‎

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,11 +1274,6 @@ invalidation/host-has-shadow-tree-element-at-nonsubject-position.html: [fail, Un
12741274
invalidation/host-has-shadow-tree-element-at-subject-position.html: [fail, Unknown]
12751275
invalidation/input-pseudo-classes-in-has.html: [fail, Unknown]
12761276
invalidation/is-pseudo-containing-sibling-relationship-in-has.html: [fail, Unknown]
1277-
invalidation/is-where-pseudo-containing-hard-pseudo-and-never-matching.html:
1278-
"subject1 color after focus is red": [fail, Unknown]
1279-
"subject2 color after focus is orangered": [fail, Unknown]
1280-
"subject3 color after focus is darkred": [fail, Unknown]
1281-
"subject4 color after focus is pink": [fail, Unknown]
12821277
invalidation/is-where-pseudo-containing-hard-pseudo.html: [fail-slow, Unknown]
12831278
invalidation/media-loading-pseudo-classes-in-has.sub.html: [fail, Unknown]
12841279
invalidation/media-pseudo-classes-in-has.html: [fail-slow, Unknown]
@@ -1299,51 +1294,32 @@ media/media-playback-state.html: [timeout, Unknown]
12991294
media/media-playing-paused-style-invalidation.html: [fail, Unknown]
13001295
media/sound-state.html: [timeout, Unknown]
13011296
nth-child-large-anplusb-clamp.html: [fail, Unknown]
1302-
nth-of-type-namespace.html: [fail, Unknown]
1303-
old-tests/css3-modsel-100.xml: [fail, Unknown]
13041297
old-tests/css3-modsel-100b.xml: [fail, Unknown]
13051298
old-tests/css3-modsel-102b.xml: [fail, Unknown]
1306-
old-tests/css3-modsel-104.xml: [fail, Unknown]
13071299
old-tests/css3-modsel-104b.xml: [fail, Unknown]
1308-
old-tests/css3-modsel-107.xml: [fail, Unknown]
13091300
old-tests/css3-modsel-107b.xml: [fail, Unknown]
1310-
old-tests/css3-modsel-111.xml: [fail, Unknown]
13111301
old-tests/css3-modsel-111b.xml: [fail, Unknown]
1312-
old-tests/css3-modsel-153.xml: [fail, Unknown]
1302+
old-tests/css3-modsel-118.xml: [fail, Unknown]
1303+
old-tests/css3-modsel-121.xml: [fail, Unknown]
13131304
old-tests/css3-modsel-159.xml: [fail, Unknown]
1314-
old-tests/css3-modsel-174a.xml: [fail, Unknown]
1315-
old-tests/css3-modsel-174b.xml: [fail, Unknown]
13161305
old-tests/css3-modsel-177a.xml: [fail, Unknown]
1317-
old-tests/css3-modsel-182.xml: [fail, Unknown]
1318-
old-tests/css3-modsel-3.xml: [fail, Unknown]
13191306
old-tests/css3-modsel-39a.xml: [fail, Unknown]
13201307
old-tests/css3-modsel-39b.xml: [fail, Unknown]
13211308
old-tests/css3-modsel-39c.xml: [fail, Unknown]
1322-
old-tests/css3-modsel-47.xml: [fail, Unknown]
1323-
old-tests/css3-modsel-57.xml: [fail, Unknown]
13241309
old-tests/css3-modsel-57b.xml: [fail, Unknown]
1325-
old-tests/css3-modsel-94b.xml: [fail, Unknown]
1310+
old-tests/css3-modsel-66.xml: [fail, Unknown]
13261311
old-tests/css3-modsel-d1b.xml: [fail, Unknown]
1327-
old-tests/css3-modsel-d3.xml: [fail, Unknown]
1328-
"old-tests/css3-modsel-{113..114}.xml": [fail, Unknown]
13291312
"old-tests/css3-modsel-{113..114}b.xml": [fail, Unknown]
1330-
"old-tests/css3-modsel-{118..121}.xml": [fail, Unknown]
1331-
"old-tests/css3-modsel-{123..124}.xml": [fail, Unknown]
13321313
"old-tests/css3-modsel-{123..124}b.xml": [fail, Unknown]
1333-
"old-tests/css3-modsel-{130..133}.xml": [fail, Unknown]
13341314
"old-tests/css3-modsel-{130..133}b.xml": [fail, Unknown]
1335-
"old-tests/css3-modsel-{137..140}.xml": [fail, Unknown]
1315+
"old-tests/css3-modsel-{132..133}.xml": [fail, Unknown]
13361316
"old-tests/css3-modsel-{137..140}b.xml": [fail, Unknown]
1337-
"old-tests/css3-modsel-{145..147}a.xml": [fail, Unknown]
1338-
"old-tests/css3-modsel-{145..147}b.xml": [fail, Unknown]
13391317
"old-tests/css3-modsel-{21..22}.xml": [fail, Unknown]
13401318
"old-tests/css3-modsel-{38..39}.xml": [fail, Unknown]
13411319
"old-tests/css3-modsel-{41..42}.xml": [fail, Unknown]
13421320
"old-tests/css3-modsel-{41..42}a.xml": [fail, Unknown]
1343-
"old-tests/css3-modsel-{50..53}.xml": [fail, Unknown]
1344-
"old-tests/css3-modsel-{65..66}.xml": [fail, Unknown]
1345-
"old-tests/css3-modsel-{91..98}.xml": [fail, Unknown]
1346-
"old-tests/css3-modsel-{96..98}b.xml": [fail, Unknown]
1321+
"old-tests/css3-modsel-{91..96}.xml": [fail, Unknown]
1322+
"old-tests/css3-modsel-{97..98}b.xml": [fail, Unknown]
13471323
parsing/invalid-pseudos.html: [fail, Unknown]
13481324
parsing/parse-anplusb.html: [fail, Unknown]
13491325
parsing/parse-attribute.html: [fail, Unknown]
@@ -2326,7 +2302,6 @@ playing-the-media-resource/!(playbackRate.html): [fail-slow, Media loading and p
23262302
DIR: html/semantics/embedded-content/the-canvas-element
23272303

23282304
2d-getcontext-options.html: [fail-with-canvas, Context options are not converted]
2329-
canvas-descendants-focusability-002.html: [fail-with-canvas, Hidden canvas descendants can be focused]
23302305
canvas-descendants-focusability-005.html: [timeout, Requires iframe srcdoc]
23312306
canvas-descendants-focusability-006.html: [timeout, Requires iframe srcdoc]
23322307
context.casesensitive.html: [fail-with-canvas, Unrecognized context names return the existing 2D context]
@@ -2538,7 +2513,7 @@ DIR: html/semantics/forms/the-datalist-element
25382513
DIR: html/semantics/forms/the-fieldset-element
25392514

25402515
HTMLFieldSetElement.html: [fail, Depends on HTMLFormElement's named properties; has dont-upstream version]
2541-
disabled-003.html: [fail, Unknown]
2516+
disabled-003.html: [fail, Need to fix focus fixup rule]
25422517
fieldset-intrinsic-size.html: [fail, Requires a layout engine]
25432518

25442519
---

‎test/web-platform-tests/to-upstream-expectations.yaml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ html/semantics/forms/the-option-element/option-ask-for-a-reset.html:
77
jsdom-only/failure-example-1.html: [fail, https://github.com/jsdom/jsdom/issues/3835]
88
jsdom-only/failure-example-2.html:
99
"failing test": [fail, https://github.com/jsdom/jsdom/issues/3835]
10-
svg/painting/parsed-svg-default-fill.html:
11-
"Default fill after inserting an XML element without the SVG namespace": [fail, https://github.com/jsdom/jsdom/issues/2688]
10+
uievents/focusevents/focusable-area.html:
11+
"Calling focus() on an inert element does nothing": [fail, inert not implemented]
12+
"Calling focus() on an element with an inert ancestor does nothing": [fail, inert not implemented]
13+
"Calling focus() on an element inside an inert shadow host does nothing": [fail, inert not implemented]
1214
uievents/focusevents/hasfocus-focus-blur.html:
1315
"document.hasFocus() returns true during internal focus shifts (blur and focus)": [fail, Need to fix document.hasFocus()]

0 commit comments

Comments
 (0)