Skip to content

[selectors] Should :not(foo) match the host of the shadow tree? #10179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
emilio opened this issue Apr 5, 2024 · 11 comments
Closed

[selectors] Should :not(foo) match the host of the shadow tree? #10179

emilio opened this issue Apr 5, 2024 · 11 comments
Labels
Closed Accepted by CSSWG Resolution selectors-4 Current Work Tested Memory aid - issue has WPT tests

Comments

@emilio
Copy link
Collaborator

emilio commented Apr 5, 2024

#9509 clarified that stuff like :is(:host) should definitely match, due to text in https://drafts.csswg.org/selectors/#data-model:

A featureless element does not match any selector at all, except those it is explicitly defined to match (and logical combination pseudo-classes representing those selectors).

So after discussing a bit with @sesse, it wasn't super-clear to me what the expected behavior is for something like this:

<!doctype html>
<div id="host" style="color: blue">
  <template shadowrootmode="open">
    <style>:not(span) { color: green !important }</style>
    What color is this text?
  </template>
</div>

I could see arguments for both behaviors:

  • On one hand, it feels very unexpected that something that doesn't contain a :host selector at all to match that host.
  • But on the other hand, it feels weird that neither span nor :not(span) match.

My read of the spec is that :not(span) should not match, because that selector is not "a logical combination pseudo-class representing those selectors [:host for simplicity]".

I think that's my preferred behavior too, because that makes it simpler to determine "can this selector match the host" (we optimize stylesheets in shadow trees to not match a lot of the rules from the host). But ultimately I could go either way I guess.

My read of the spec doesn't match @sesse, and it seems at least the spec could get a clarification of what that "representing those selectors" means. Maybe "containing those selectors", if my read is correct, or just removing that text, if @sesse's is?

cc @tabatkins @rniwa

@emilio emilio added selectors-4 Current Work Agenda+ labels Apr 5, 2024
@lilles
Copy link
Member

lilles commented Apr 5, 2024

An author reason for not matching the host with :not(foo):

Say I want to style <img> elements in the shadow tree that's not wrapped in some .foo:

:not(.foo) img { ... }

If :not(.foo) matches the shadow host, that selector would always match all img elements.

I would instead have to write:

:host :not(.foo) img { ... }

or add some other selector outside :not() that I knew matched the element on which I set .foo.

@emilio
Copy link
Collaborator Author

emilio commented Apr 5, 2024

Yeah that's a really good point.

@LeaVerou
Copy link
Member

LeaVerou commented Apr 9, 2024

I think it’s important to preserve the expectation that foo, :not(foo) = *.

The img example could easily be img:not(foo *)

@emilio
Copy link
Collaborator Author

emilio commented Apr 9, 2024

Well, * wouldn't match the host.

@Loirooriol
Copy link
Contributor

Consider :not(:not(:host)). I think it would be very unexpected for :not(:host) to match the host. Thus for both :not(:not(:host)) and :not(.foo) the argument is something that doesn't match the host, so the behavior should be consistent.

Logically it can be a bit strange if :host matches but :not(:not(:host)) doesn't?

I think the possibilities are:

  1. :not() matches the host if none of its arguments matches the host.
  2. :not() never matches the host.
Selector Option 1 Option 2
:not(:host)
:not(:not(:host))
:not(.foo)
:not(:host, .foo)
:not(:host.foo)
:not(:not(:host), .foo)
:not(:not(:host)):not(.foo)
:host:not(.foo)

@emilio
Copy link
Collaborator Author

emilio commented Apr 9, 2024

I think there might be a third option which even though it's a bit weird would be an improvement on (1) if we go that route, which is conditioning matching it on the selector having a :host selector in some way (which was my read of the spec). Depending on how we define that, it could make :not(:not(:host)):not(.foo) not match, but it'd also make :not(.foo) not match, which is IMO the preferred behavior (both for performance and for matching author expectations).

@sesse
Copy link
Contributor

sesse commented Apr 9, 2024

This has some pitfalls, though: The mere presence of :host would influence the rest of the selector. We probably don't want this situation:

  • :not(.foo): false (no :host in selector, so not considered for match)
  • :not(:host): false (:host in selector, so considered for match, but the selector itself does not match)
  • :is(:not(.foo), :not(:host)): true!

Not to mention the forward-compat issues we had with nesting, where people were worried about what would happen if something unknown to the browser (from a future spec) was nest-containing. You have a similar problem here with “host-containing”.

TBH I'm not too worried about performance here; it's just one more element to match in a potentially very long chain, and if you want to optimize that out by checking for :host, you can just as easily drop the optimization if there's a :not in there (which is fairly rare).

@emilio
Copy link
Collaborator Author

emilio commented Apr 9, 2024

:is(:not(.foo), :not(:host)): true!

Not necessarily, right? The :not(.foo) wouldn't match (again, depending on how we define this), because it doesn't contain :host, and thus the whole thing would be false?

@Loirooriol
Copy link
Contributor

So if I understand correctly, your proposal would be that a selector needs to fall into one of these cases:

  • It's not allowed to match the host
  • It's allowed to match the host, but doesn't match it
  • It matches the host

Then, for simple selectors:

  • :host matches the host
  • :is() / :where() matches the host if some of its arguments matches the host, otherwise it's allowed to match the host but doesn't match it if some of its arguments is allowed to match the host but doesn't match it, otherwise it's not allowed to match the host.
  • :not() is allowed to match the host but doesn't match it if some of its arguments matches the host, otherwise it matches the host if some of its arguments is allowed to match the host but doesn't match it, otherwise it's not allowed to match the host.
  • Other simple selectors aren't allowed to match the host.

For selector lists: same as :is() / :where().

I'm less sure about compound and complex selectors:

  • Should :not(.foo:host) match even though :not(.foo), :not(:host) doesn't?
  • Should :not(:host > .foo) match even though :root.foo, :not(:host) > .foo, :host > :not(.foo) doesn't?

@tabatkins
Copy link
Member

Whoops, sorry for missing this!

For the general behavior, @lilles got it exactly right - the point of featurless-ness is to make it so that authors don't have to think about the host elements most of the time and will still get the likely intended behavior, so :not(.foo) should not match a host element, for the same reason * doesn't.

(In other words, Lea's request that X, :not(X) be equivalent to * is still preserved, since in both cases the host element isn't matched so long as X isn't :host.)

For the more complex cases, I hadn't actually thought thru those cases when writing up the feature, but I think @Loirooriol's breakdown works well.

For the more complex :not() cases, I think we might want to add the following:

  • Compound selectors are allowed to match the host only if all the contained simple selectors are allowed to match the host.
  • Complex selectors are allowed to match the host only if the subject compound selector is allowed to match the host. (So in :host > .foo, the first compound selector is allowed to match the host (and does), but the complex selector as a whole isn't allowed to match the host.)

This would make both of the complex :not() cases "not allowed to match the host".

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [selectors] Should :not(foo) match the host of the shadow tree?, and agreed to the following:

  • RESOLVED: edit in what's described in Tabs last comment
The full IRC log of that discussion <RRSAgent> I have made the request to generate https://www.w3.org/2024/08/14-css-minutes.html fantasai
<keithamus> TabAtkins: a q raised: what the `:not` pseudo class means wrt the `:host` element. The point of it being featureless is to avoid having to defensively think about what the outer page is doing with the host element
<keithamus> ... should :not match everything but the host by default?
<keithamus> ... not should, by default, not match the host element by default. Just like .foo wouldn't match, not(.foo) wouldn't.
<keithamus> ... if you explicitly mention the host in the :not, you're explicitly opting in, so there could be a small set of rules for :not matching, compound selectors matching, complex selectors are allowed to match only if the subject compound is allowed to match
<keithamus> ... this captures the notion of if the :not selector is caring about the host element. If so it is allowed to match otherwise it ignores like everything else.
<keithamus> ... if this sounds reasonable I can write the edits
<emilio> q+
<astearns> ack emilio
<keithamus> ... I believe that brings all logical combinator pseudos into a reasonable state wrt the host eleemnt
<keithamus> emilio: the only selector that would matter would be :not(:host)? Other stuff would never match effectively?
<keithamus> TabAtkins: I believe so
<keithamus> emilio: can we make this simpler and say it never matches? Given :host is featureless
<keithamus> ... is there a use case for :not(:host)?
<keithamus> TabAtkins: there are other things that could do that. Also :has(). You could potentially match a host element without :host, and if you :not that, you could potentially match the host...
<keithamus> ... example:
<TabAtkins> if :has(.foo) doesn't match the host (but is allowed to), the :not(:has(.foo)) would match the host
<keithamus> emilio: do you really want :not(:has(.foo)) to really match?
<keithamus> TabAtkins: that's the next issue
<keithamus> emilio: I thought the next issue was :host(:has work
<keithamus> emilio: I think I see, it's very weird. In general I dont think you can match the host with something which doesn't contain :host
<keithamus> TabAtkins: that's the next issue
<keithamus> astearns: Could we move forward with the complicated bits and drop them if we dont need them?
<keithamus> TabAtkins: a selector X that's potentially able to match a set of elements, X and :not(X) should match all elements, which may or may not include the host.
<keithamus> ... that's the underpinning I want to resolve on
<TabAtkins> s/match all elements/match all those potential elements/
<keithamus> PROPOSED RESOLUTION: edit in what's described in Tabs last comment
<keithamus> RESOLVED: edit in what's described in Tabs last comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed Accepted by CSSWG Resolution selectors-4 Current Work Tested Memory aid - issue has WPT tests
Projects
Status: Unsorted
Development

No branches or pull requests

7 participants