[csswg-drafts] Mixing :is() (or equivalent) with pseudo-elements (#9702)

tabatkins has just created a new issue for https://github.com/w3c/csswg-drafts:

== Mixing :is() (or equivalent) with pseudo-elements ==
The original version of `:is()`, `:-webkit-matches()`, allowed pseudo-elements in its argument list. So you could write `.foo:-webkit-matches(*, ::before, ::after)`, and it would be equivalent to `.foo, .foo::before, .foo::after`.

We ended up removing this, because it conflicts with the Selectors data model - simple selectors, like pseudo-classes, only ever *filter* the current set of matched elements; only combinators can *change* the set to new elements (and pseudo-elements). The pseudo-element "selector" is a legacy syntax mistake from the early days of CSS, and today would have been instead done as a type of combinator (as discussed in #7346, which still might happen).

But this means that you can't easily write useful selectors like the above. You *can* write them with Nesting, but you can't then nest *further*: `.foo { *, &::before, &::after {...}}` is fine, but `.foo, .bar::before { &:hover {...}}` won't match any hovered ::before pseudos, despite `.bar::before:hover` being a valid and meaningful selectors, because it's treated identically to `:is(.foo, .bar::before):hover`, which drops the `.bar::before` arg from the `:is()`.

This isn't great! And it makes Nesting less useful for future cases of nested pseudo-elements, in addition to anything built on Nesting concepts, like Mixins probably will be.

I don't have an answer ready for this, I just needed to raise it for broader discussion and thought.

---------

Some loosely-ordered thoughts:

* The three syntactic elements of Selectors are simple selectors (filter the matched elements), combinators (change the matched elements), and *lists* (union the matched elements).
    * Possibly we recognize a fourth - the "complex selector unit" which filters *and* might change the subject to a pseudo-element.
* `:is()` is trying to replicate the *functionality* of lists, but pulling it into the syntactic space of simple selectors, thus the conflict. You can combine `:is()` with other simple selectors, so the final subject of each selector inside of `:is()` needs to be the same element.
* If we did allow pseudo-elements in there *and* kept the data model correct, it would be unintuitive; we'd effectively have some selector reordering. That is, `:hover:is(*, ::before)` would have to be equivalent to `:hover, ::before:hover`, not `:hover, :hover::before`.
* But that sort of re-ordering is in conflict with things like `.foo:is(::before, ::after)`, wanting to avoid repeating the preceding selector. And simple selector order can't matter; `.foo:is()` and `:is().foo` have to be identical. So that also prevents reasonable stuff like `.foo:is(::before, ::after):hover` from working.
* I feel like we need to somehow invent a way to wedge "lists" into the syntax of a single selector, rather than being restricted to the top level of a selector as it is now. The problem isn't the branching that :is() allows, it's the fact that :is() is a simple selector, so we can't care about order/etc.
* Spitballing: naked parens indicate a list. Lists can change the subject, and stuff can't get reordered across them. Like `.foo(*, ::before, ::after):hover`.
* Nope, `foo(` is a function token. I need the parens to be separate from other bits of the compound selector, but I also need to still be able to glom a compound selector before or after it.
* Second try: `.foo = (*, ::before, ::after) = :hover`. 
    * `=` is a new combinator that *doesn't* change the set of matched elements at all; it's a no-op. 
    * `(...)` is a new term in the `<complex-selector-unit>` grammar, that takes a selector list and matches any of them. Since it's part of a complex selector and *guaranteed* to be surrounded by combinators, we avoid the issues from earlier.
    * So in this example, we (1) select `.foo` elements, (2) without changing the set of matched elements, select `*, ::before, ::after` elements (aka the element, plus their before/after pseudos), (3) without changing the set of matched elements, select `:hover` elements.
    * This works with other combinators, too: `.foo > (*, ::before)` selects all children of a `.foo` element *and* their before pseudos.
    * The selectors inside the `()` can be full complex selectors, just like in `:is()`, so `.foo = (.bar *, #baz > *)` selects `.foo` elements that have a `.bar` ancestor or a `#baz` parent.
    * This is still slightly weaker than Nesting itself; you can't do the equivalent of `.foo { &, & > .bar {...}}` because the current match set (whatever the preceding combinator yielded) is always the subject of the selectors in the paren list.  That's probably okay. We'd have to invent a new way to refer to the elements being matched if we wanted to expand that (not `:scope` or `&`, but a secret third way). I dunno, maybe we do need it.
    * Specificity would have to be the same as `:is()`, to avoid the same sort of "which way did it match" exponential explosion.
    * Nesting can then define itself on top of this - `X { Y {...}}` desugars to `(X) = (Y) {...}`, with the magic ability to use `&` to explicitly refer to the current match elements. If I'm thinking about this correctly, this is a no-op change *except* that it allows `::before, ::after { &:hover {...}}` to work, where today it doesn't.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/9702 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 12 December 2023 23:12:20 UTC