Skip to content
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

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

Open
emilio opened this issue Apr 5, 2024 · 10 comments
Open

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

emilio opened this issue Apr 5, 2024 · 10 comments
Labels

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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Unsorted
Development

No branches or pull requests

6 participants