Bug 233380 - CSS :scope pseudo selector doesn't work in shadowRoot.querySelectorAll
Summary: CSS :scope pseudo selector doesn't work in shadowRoot.querySelectorAll
Status: RESOLVED INVALID
Alias: None
Product: WebKit
Classification: Unclassified
Component: CSS (show other bugs)
Version: WebKit Nightly Build
Hardware: All All
: P2 Normal
Assignee: Nobody
URL:
Keywords:
Depends on:
Blocks: 148695
  Show dependency treegraph
 
Reported: 2021-11-19 13:18 PST by Andrei Anischevici
Modified: 2021-11-22 05:26 PST (History)
4 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Andrei Anischevici 2021-11-19 13:18:22 PST
CSS :scope pseudo selector doesn't work in querySelectorAll() when it is called on the shadowRoot of a custom element.
Due to this bug, it's impossible to select direct descendants of the shadowRoot via CSS selector.

Sample code to reproduce the issue:

-------------------------------

  const content = `
    <div>
      <input type="radio" id="btn1" name="btn1" value="Button 1">
      <label for="btn1">Button 1</label><br>
      <div>
        <input type="radio" id="btn2" name="btn2" value="Button 2">
        <label for="btn2">Button 2</label><br>
      </div>
    </div>
  `;
  customElements.define(
    "my-button",
    class extends HTMLElement {
      constructor() {
        super();

        const shadowRoot = this.attachShadow({ mode: "open" });
        shadowRoot.innerHTML = content;
      }
    }
  );

  const container = document.createElement("DIV");
  container.setAttribute("id", "container");
  container.innerHTML = `
    <my-button>FirstButton</my-button>
    <my-button>SecondButton</my-button>
  `;

 document.body.appendChild(container);

 document.body.querySelectorAll(':scope > DIV'); // works

 document.getElementById('container').children[0].shadowRoot.querySelectorAll(':scope > DIV') // fails, [] is returned instead of [div]

-------------------------------

See also related Firefox issue - https://bugzilla.mozilla.org/show_bug.cgi?id=1689893

Note: this is working properly in Chrome.
Comment 1 Ryosuke Niwa 2021-11-19 20:46:44 PST
As mentioned in Mozilla bug, this seems working as intended. If this behavior is desirable, perhaps you need to open a spec issue for CSS WG.
Comment 2 Andrei Anischevici 2021-11-20 09:15:22 PST
(In reply to Ryosuke Niwa from comment #1)
> As mentioned in Mozilla bug, this seems working as intended. If this
> behavior is desirable, perhaps you need to open a spec issue for CSS WG.

As I also mentioned in the Mozilla bug, this is not working as intended and the comment with which that bug was initially resolved does not apply, as we're not crossing the shadow boundary with a single selector, but instead doing the selection from the shadow root node, which is expected to be working, similarly to how it's working for the document root node. That ticket has since been reopened and being looked into.

As I also noted, this is working properly in Chrome and it is indeed a bug in Mozilla and WebKit, as there's no way of selecting direct descendants of the shadow root, other than parsing the selector and manually matching each shadow root child.
Comment 3 Ryosuke Niwa 2021-11-20 09:26:17 PST
(In reply to Andrei Anischevici from comment #2)
> (In reply to Ryosuke Niwa from comment #1)
> > As mentioned in Mozilla bug, this seems working as intended. If this
> > behavior is desirable, perhaps you need to open a spec issue for CSS WG.
> 
> As I also mentioned in the Mozilla bug, this is not working as intended and
> the comment with which that bug was initially resolved does not apply, as
> we're not crossing the shadow boundary with a single selector, but instead
> doing the selection from the shadow root node, which is expected to be
> working, similarly to how it's working for the document root node. That
> ticket has since been reopened and being looked into.

Well, :scope is spec'ed to behave like this. node.querySelector is defined here:
https://dom.spec.whatwg.org/#dom-parentnode-queryselector

which invokes the algorithm "to scope-match a selectors string" with "this", which is shadow root in this case:
https://dom.spec.whatwg.org/#scope-match-a-selectors-string

This algorithm in turn does this:
"Return the result of match a selector against a tree with s (the parsed selector) and node’s root (shadow root's root is itself) using scoping root node (shadow root)"

Now take a look at the definition of a scoping root:
https://drafts.csswg.org/selectors-4/#scoping-root
"The root of the scoping subtree is called the scoping root, and may be either a true element (the scoping element) or a virtual one"

And now the definition of ":scope":
https://drafts.csswg.org/selectors-4/#the-scope-pseudo

>The :scope pseudo-class represents any element that is a :scope element. If the :scope elements are not explicitly specified, but the selector is scoped and the scoping root is an element, then :scope represents the scoping root; otherwise, it represents the root of the document (equivalent to :root). Specifications intending for this pseudo-class to match specific elements rather than the document’s root element must define either a scoping root (if using scoped selectors) or an explicit set of :scope elements.

Here, the spec says that if the scoping root is not an element, we must use the root of the document, which is a document. Since shadow root is not a document, we would not match.

Now, it's possible that this is an editorial error in CSS selectors 4 and we want to say that it matches the root node (i.e. shadow root in the case). But as far as I read the current set of specifications, the current behavior of WebKit and Gecko are correct and Chrome's behavior is the one that is not spec compliant.
Comment 4 Andrei Anischevici 2021-11-20 11:31:58 PST
(In reply to Ryosuke Niwa from comment #3)

Thank you for the prompt response, I really appreciate the detailed reply!

> This algorithm in turn does this:
> "Return the result of match a selector against a tree with s (the parsed
> selector) and node’s root (shadow root's root is itself) using scoping root
> node (shadow root)"

> >The :scope pseudo-class represents any element that is a :scope element. If the :scope elements are not explicitly specified, but the selector is scoped and the scoping root is an element, then :scope represents the scoping root; otherwise, it represents the root of the document (equivalent to :root). Specifications intending for this pseudo-class to match specific elements rather than the document’s root element must define either a scoping root (if using scoped selectors) or an explicit set of :scope elements.
> 
> Here, the spec says that if the scoping root is not an element, we must use
> the root of the document, which is a document. Since shadow root is not a
> document, we would not match.

Please allow me to politely disagree and suggest that this a misinterpretation of the spec, specifically this part:
> "but the selector is scoped and the scoping root is an element, then :scope represents the scoping root"

The shadow root IS an element, although it's a virtual element (since it's a DocumentFragment), it's still an element, according to the scoping root definition here https://drafts.csswg.org/selectors-4/#scoping-root

"The root of the scoping subtree is called the scoping root, and may be either a true element (the scoping element) or a virtual one (such as a DocumentFragment)"

"virtual one" == "virtual element"

I totally agree that this "and the scoping root is an element" part in the ":scope" spec is somewhat confusing and should've been excluded altogether, since from the "scoping root" definition it's clear that it can't be anything else than an element..

Furthermore, if we look away from the specs for a bit and take a look at it from a purely logical standpoint:
 - Given a node tree, in this case a DocumentFragment (which supports querySelectorAll() in its API https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment/querySelectorAll) I need to be able to select the direct descendants of the tree root, i.e. the top-level elements using some selector. Currently ':scope > ...' is the only selector that can accomplish this and it's a real pain to work around this if it's not supported..

I see that there's a new ::shadow pseudo-element proposed that would allow an alternate solution to this issue
https://www.w3.org/TR/css-scoping-1/#shadow-pseudoelement
however, none of the browsers seem to implement it at this point..
Comment 5 Andrei Anischevici 2021-11-20 12:03:27 PST
(In reply to Andrei Anischevici from comment #4)
> I see that there's a new ::shadow pseudo-element proposed that would allow
> an alternate solution to this issue
> https://www.w3.org/TR/css-scoping-1/#shadow-pseudoelement
> however, none of the browsers seem to implement it at this point..
Actually this no longer applies, since that suggestion is from the 2014 CSS Scoping Module Level 1 spec, and in the latest 2021 spec the ::shadow pseudo-element can no longer be found https://drafts.csswg.org/css-scoping/

https://developers.google.com/web/updates/2017/10/remove-shadow-piercing

So, the only viable solution to the issue is to fix the ":scope" pseudo-class so it works on shadow roots.
Comment 6 Andrei Anischevici 2021-11-20 12:13:13 PST
Additionally, in the "Matching Selectors Against Shadow Trees" section of the Scoping spec at https://drafts.csswg.org/css-scoping/#selectors-data-model
there's this note:

"When a selector is matched against a tree, its tree context is the root of the root elements passed to the algorithm. If the tree context is a shadow root, that selector is being matched in the context of a shadow tree.

For example, any selector in a stylesheet embedded in or linked from an element in a shadow tree is in the context of a shadow tree. So is the argument to querySelector() when called from a shadow root."

Since the selector containing ":scope" is being matched in the context of the shadow tree, as specified here, it is expected that ":scope" would resolve to the shadow root, and not to the document root.
Comment 7 Emilio Cobos Álvarez (:emilio) 2021-11-20 12:21:29 PST
I'll go through this and the relevant Firefox bug next Monday, but in any case a document fragment never matches any selector. In the context of a shadow tree only :host can match the shadow host, and it is a featureless element so it should not match scope.

It could match :host(:scope) though, maybe... I haven't checked whether that works in any browser.
Comment 8 Emilio Cobos Álvarez (:emilio) 2021-11-22 03:01:19 PST
I think this is invalid. You can use :host > div instead, which ought to work per spec. I filed https://bugs.chromium.org/p/chromium/issues/detail?id=1272434 for Chromium.
Comment 9 Andrei Anischevici 2021-11-22 04:51:00 PST
(In reply to Emilio Cobos Álvarez (:emilio) from comment #8)
> I think this is invalid. You can use :host > div instead, which ought to
> work per spec. I filed
> https://bugs.chromium.org/p/chromium/issues/detail?id=1272434 for Chromium.

Thank you for looking into this, Emilio!

I have just tested the ':host > div' solution that you suggested, and it does indeed accomplish the selection of direct descendants across all browsers (excluding IE, which is expected), so we have a path forward..

If :scope is indeed not supposed to be working for shadow roots, I'd suggest improving the CSS Selectors Level 4 spec, so that this is mentioned explicitly in :scope and there's no ambiguity:

"If the :scope elements are not explicitly specified, but the selector is scoped and the scoping root is an element, then :scope represents the scoping root;"

change to =>

"If the :scope elements are not explicitly specified, but the selector is scoped and the scoping root is a true (not virtual) element, then :scope represents the scoping root;"

would you agree?
Comment 10 Emilio Cobos Álvarez (:emilio) 2021-11-22 05:26:09 PST
Yeah, that seems reasonable. Maybe file it in https://github.com/w3c/csswg-drafts/issues/new?