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-4]Why Pseudo-elements cannot be represented by the :is()/:matches()? #2284

Open
yisibl opened this issue Feb 7, 2018 · 11 comments

Comments

@yisibl
Copy link
Contributor

yisibl commented Feb 7, 2018

Pseudo-elements cannot be represented by the matches-any pseudo-class; they are not valid within :matches().

https://www.w3.org/TR/2018/WD-selectors-4-20180201/#matches

Safari support :matches(::before)

The :matches() has now been renamed to :is()

https://drafts.csswg.org/selectors/#selectordef-matches

@Loirooriol
Copy link
Contributor

I guess it can be useful in something#very.long :matches(::before, ::after). But these pseudo-elements don't have internal structure and thus can't be followed by a combinator, so maybe a :matches that contains such pseudo-elements should neither.

@tabatkins
Copy link
Member

The reason is that pseudo-elements, semantically, contain a combinator in them - if you start with .foo, extending to .foo.bar gives you a subset of the same elements, while extending to .foo::bar gives you something completely different, like .foo > .bar would.

:matches(), on the other hand, is definitely a pseudo-class - it should only return a subset of the elements that the selector without :matches() returns. Pseudo-elements thus don't really make sense in there.

Further, say you have something like .foo:matches(::before):hover - what does that mean? Is it the same as .foo:hover::before, or .foo::before:hover (those are different and both valid!)? Normally we can arbitrarily re-order the non-pseudo-element simple selectors in a compound selectors, including pseudo-classes, but if ::before is allowed maybe we wouldn't be able to do that any more?

@SelenIT
Copy link
Collaborator

SelenIT commented Feb 8, 2018

It seems to me that since pseudo-elements were allowed to be effectively the subject of pseudo-classing, we can't restrict the effect of pseudo-classing to "subsetting the elements only" anymore. Yes, currently only pseudo-classes of user action are allowed for pseudo-elements, but I agree that @Loirooriol's example could be very useful in many cases.

Maybe it would be useful to introduce something like "any pseudo-element" notation that would be combinable with pseudo-classes like :mathches(), :not() and probably :is() and would make the meaning of the selector unambiguous? E.g. something like .somewhere .something::*:matches(::before, ::after):hover (meaning that :hover belongs to the pseudo-element part, not the originating element)? And simply *:matches() would always match only elements, so :matches(::before) would match nothing, just like other valid-but-meaningless selectors like a:matches(b).

@tabatkins
Copy link
Member

It seems to me that since pseudo-elements were allowed to be effectively the subject of pseudo-classing, we can't restrict the effect of pseudo-classing to "subsetting the elements only" anymore. Yes, currently only pseudo-classes of user action are allowed for pseudo-elements, but I agree that @Loirooriol's example could be very useful in many cases.

If I'm reading you correctly, I think you misunderstood what I meant by my previous comment. I'm not drawing any real distinction between elements and pseudo-elements; as far as Selectors is concerned, they're basically the same thing. (This is my entire point above; because a pseudo-element is effectively a new element, the pseudo-element selector is not equivalent in meaning to any other simple selector, and is instead basically a combinator into the "pseudo-children" of an element.)

Maybe it would be useful to introduce something like "any pseudo-element" notation that would be combinable with pseudo-classes

I wouldn't mind recasting things a bit to add a real combinator into the "pseudo-children", where presumably the pseudo-element names are instead matched as tagnames, like .foo :: before:hover, or .foo :: *:matches(before, after), etc. I've looked into this before and there were some issues with parsing if we just reinterpreted :: as a combinator; I'll have to dig up what they were, as I can't recall right now.

@SelenIT
Copy link
Collaborator

SelenIT commented Feb 9, 2018

@tabatkins I don't say that elements and pseudo-element are fundamentally different, and I like your interpretation of the :: syntax as a "combinator", but in the same time I tend to see them as something conceptually close (though not the same, of course) to the elements from a different namespace. It's based on the fact that the usual universal selector (*) doesn't select pseudo-elements by default, so we have to list them explicitly like *, *::before, *::after (this is quite popular, for example, for changing the default box-sizing to border-box globally). And I assume that :matches() is equivalent to *:matches(), which leads me to conclusion that element:matches(::pseudo-element) should match nothing because a particular element and its particular pseudo-element can't be the same object.

And I like the idea of treating :: as a combinator! It seems to be consistent with the concept of Shadow DOM combinators. Perhaps only "legacy" pseudo-elements with single colon syntax are the parsing issue in this case?

@tabatkins
Copy link
Member

tabatkins commented Feb 9, 2018

Ah, I dug up the old thread (from 2012!).

The parsing problem with reinterpreting :: as a combinator is indeed lethal. It's a one-two punch of backwards-compatibility.

First, selectors like .foo > ::before exist today. If :: is a combinator, then that's two combinators in a row. We'd have to additionally add a new ability to selectors - if you have multiple combinators in a row, it implies a * selector between them. That way, the previous selector is identical to foo > * :: before, which has the meaning we want.

However, this requires a new constraint - the descendant combinator (at least in its current whitespace form) can't play with this new chained-combinators feature, or else .foo > .bar would suddenly start interpreting as three combinators in a row, meaning .foo * > * .bar. That's not right! So we'd have to add in a new non-whitespace version of the descendant combinator to allow it to work with this feature.

But this causes another problem! Selectors like .foo ::before exist today. Today, that's equivalent to .foo *::before; with the new combinator-chaining feature and the descendant-combinator restriction, it changes meaning to become .foo :: before (the before pseudo-element of the .foo element). This isn't acceptable!

So, we're screwed. Some existing selectors require a new feature to be parsed the same, which requires a funky restriction, which causes other existing selectors to not parse the same.

The only way around this is to add a totally new combinator for pseudo-children, not ::, and that's much harder to sell as being worthwhile. :(

@fantasai fantasai added the selectors-4 Current Work label Feb 14, 2018
@fantasai fantasai added selectors-5 and removed Agenda+ selectors-4 Current Work labels Dec 6, 2018
@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed Why \"Pseudo-elements cannot be represented by the matches-any pseudo-class\"?, and agreed to the following:

  • RESOLVED: Defer this issue to L5
The full IRC log of that discussion <dael> Topic: Why \"Pseudo-elements cannot be represented by the matches-any pseudo-class\"?
<dael> github: https://github.com//issues/2284
<dael> fantasai: I wanted to ask if this is something we should try and define or close as no change
<dael> TabAtkins: Valid.
<dael> TabAtkins: I know that safari allows pseudo elements, but it doesn't make sense and I don't know why
<TabAtkins> .foo:matches(::before):hover
<dael> TabAtkins: This selector ^ bothers me.
<dael> TabAtkins: Unclear if it means before when foo is hovered or when foo.before is hovered.
<TabAtkins> .foo:matches(::before).bar is invalid, then
<dael> fantasai: If we're going to define we have to define if a selecotr ends in a pseudo element and has same meaning as if you placed element outside of matches.
<TabAtkins> .foo:matches(::before, .baz).bar is invalid, too
<dael> fantasai: That's invalid, right.
<dael> TabAtkins: Reasonable sort of thing to define on
<dael> TabAtkins: It's weird and I don't like it, but if it's what we want I'm okay
<dael> ericwilligers: What's the need for this?
<TabAtkins> .foo:matches(::before, ::after)
<dael> TabAtkins: Safari does it for selectors like ^
<dael> TabAtkins: You want to assign a style to multiple psuedo elements. Can't do that with matches right now. That's annoying, I agree
<dael> TabAtkins: Reasonable thing. Even bikeshed stylesheet would like to do a selector like this.
<fantasai> s/matches/is/ btw ;)
<TabAtkins> .foo:matches(::before, .bar)
<TabAtkins> ^ invalid
<dael> TabAtkins: Another option is we're more limited and you can put psuedo elements in if they're the only thing in the selector. All options have to be pseudo elements
<TabAtkins> .foo:matches(::before:hover) is valid
<dael> TabAtkins: That would be okay way to deal
<plinss> .foo:matches(::before):matches(:hover) ?
<TabAtkins> would be valid by my rule I think
<dael> fantasai: Thing that's tricky is different pseudo elements can be followed by different things. No need to make first example invalid b/c each branch is valid. If any branch is invalid whole is invalid.
<dbaron_> Pseudo elements seem like they might differ in what is allowed after them
<dael> fantasai: More restrictive I'm not sure that gains us a whole lot. You'll still need contextual checking.
<fantasai> s/checking/checking, even if they're all pseudo-elements/
<dael> TabAtkins: ericwilligers we don't support pseudo elements in matches right now, correct?
<dael> ericwilligers: Correct
<dbaron_> It seems like you might want to ensure that all branches of the :matches end in the same restriction state
<dael> TabAtkins: FF or Edge, how does this sound. Should I close no change and Safari violates spec? Or do we want?
<TabAtkins> dbaron_, I agree with that as a good restriction if we go this way
<dael> Rossen: Curious to hear from Safari folks.
<dael> Rossen: dbaron_ is on IRC [reads]
<dbaron_> (where we need to be conservative about what is the same, e.g. before and after OK but not selection)
<TabAtkins> :matches(::before, ::spelling-error) would be invalid
<dael> TabAtkins: before and after are fine, but before and spelling wouldn't work because allow different things
<dael> ericwilligers: Safari uses different selector name. So it doesn't matter too much.
<dael> TabAtkins: Not a backwards compat concern. It's what we want to do for the future
<dael> TabAtkins: I'm perfectly fine saying close no change
<dbaron_> Still have mixed feelings about whether to allow pseudos in v1
<dael> TabAtkins: dbaron_ is unsure. We can relax restriction later
<dbaron_> (typing on phone, BTW)
<dael> Rossen: smfr said Safari folks can't follow because they're in a meeting
<dael> Rossen: Objections to resolving no change?
<dael> fantasai: I think deferred
<dael> fantasai: Close no change means we don't want to accept. We might accept in future
<dael> fantasai: I'd prefer...if we're going to consider in the future we leave it open for selectors 5. If it's a baad idea we close no change.
<dael> Rossen: Don't disagree
<dael> Rossen: objections to defer to L5?
<dael> RESOLVED: Defer this issue to L5
@tabatkins
Copy link
Member

(Copied from #4417)

In the past I've resisted things like .foo:is(.bar, ::before), because the ::before part is technically changing the subject of the compound selector, something no other selector can do.

But this just leaves a functionality gap, since pseudo-elements don't have a combinator dedicated to them to allow matching plainly. Selector syntax is awkwardly designed around pseudo-elements, but we're stuck with it, and should accept that pseudo-elements are part of the compound selector grammar and are allowed to change the subject in a real way.

Thus we should make sure that :not()/etc work properly with them too.

(There's no reason to use :not() with any of the current pseudos, but ::part(foo):not(::part(bar)) makes sense and is useful.)

@ExE-Boss
Copy link
Contributor

I think ::*:is(before, after) might be a solution to avoiding changing the subject of the compound selector, and in a way that would be compatible with :: being a combinator.

@SelenIT
Copy link
Collaborator

SelenIT commented Nov 2, 2020

Rethinking the "nature" of pseudo-elements in terms of selector syntax (#5676), a new way to resolve the contradiction came to my mind...

the descendant combinator (at least in its current whitespace form) can't play with this new chained-combinators feature, or else .foo > .bar would suddenly start interpreting as three combinators in a row

What if we declare the whitespaces around combinators other than :: the optional part of these combinators themselves (i.e., the > combinator would be defined as /\s*\>\s*/ in RegExp terms, and so on, with only :: remaining simply /::/ for compat reasons)?

This change seems to make all the examples above unambiguous and retaining their current meaning: e.g. > will always remain a single combinator regardless the number of whitespaces around it (unless these whitespaces are separated by an explicit *), while > :: will be parsed a sequence of two combinators, > and ::, leading to insertion of the implicit * (by the new rule).

@cdoublev
Copy link
Collaborator

cdoublev commented Nov 23, 2022

Selector syntax is awkwardly designed around pseudo-elements, but we're stuck with it, and should accept that pseudo-elements are part of the compound selector grammar and are allowed to change the subject in a real way

It would be weird to allow ::marker to change the subject in ::before:is(::marker), but you may already have solution(s) in mind for this case.

After some reflection on this, I think a pseudo-element should be allowed in logical combination pseudo-classes when the logical combination pseudo-class is compounded to the same pseudo-element or ** (theoretical universal element/pseudo-element selector). For example, ::before:not(#id::before:focus, :hover) should be valid. :not(::before, ::after) and :not(::before, div) should be invalid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
8 participants