w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 657 forks source link

[selectors] Child & descendant pseudo element combinators #7346

Open jakearchibald opened 2 years ago

jakearchibald commented 2 years ago

Right now, div::before::marker means: Select the ::marker pseudo elements, that are a child of a ::before pseudo element, that originate from a div.

There isn't a way to say: Select the ::marker pseudo elements, that originate somewhere within the pseudo element tree of a div.

It'd be great if we had a way to achieve this, such as:

Future additions could include other combinators prefixed with :, such as :+, to target adjacent sibling pseudo elements.


This use-case came up in shared element transitions. Over there, we create pseudo-trees that are more complicated than (I think) we've seen in other features:

html
└─ ::page-transition(id)
   └─ ::image-wrapper
      ├─ ::outgoing-image
      └─ ::incoming-image

Our current model is to expose these through the following selectors:

However, these don't really communicate the structure, and won't play well with upcoming features like nesting, or :has.

We'd like to expose the pseudos as they're structured, so instead of:

::page-transition-outgoing-image(id) { /*…*/ }

It would be:

::page-transition(id) :>> outgoing-image { /*…*/ }

Which plays well with nesting:

::page-transition(id) {
  & :>> outgoing-image { /* … */ }
  & :>> incoming-image { /* … */ }
}

This would also allow for selectors like:

::page-transition(id):not(:has(:>> incoming-image)) :>> outgoing-image {
  /* … */
}

Note: According to https://github.com/w3c/csswg-drafts/issues/7463, the above won't be possible.

…which selects the outgoing-image pseudo element within a page-transition pseudo element, that doesn't also have an incoming-image. Although, for that particular case, I hope we can make something like this work:

::page-transition(id) :>> outgoing-image:only-child {
  /* … */
}
Loirooriol commented 2 years ago

Related: #4565

jakearchibald commented 2 years ago

Interesting! So with this proposal:

*, * :>> * { box-sizing: border-box }

…would apply border-box to everything.

tabatkins commented 2 years ago

I hashed this out with Jake last week, and like this proposal; it addresses several of the pain points I've run into with pseudo-element syntax over the years.

Small detail, probably obvious from the examples: when selecting into the "pseudo tree" of an element, the pseudo-elements are treated as having tagnames equal to their names, so you use type selectors to target them. div :> before hits a ::before pseudo, for example. This requires us to broaden the syntax of Selectors slightly to allow type selectors to also be functions, for things like div :> slotted(.foo), but that shouldn't be problematic.

Also, in chained pseudo-elements like ::before::marker, the ::marker is part of the pseudo tree" of the before, so you do have to write ` :> before :> marker; :> before > markerwon't work, as the before pseudo-element doesn't have achild element. The pseudo-descendant combinator:>>just descends down the pseudo-children links, so |>> marker` grabs all markers, even those of pseudo-elements.

Jake's listed motivations are all great, and why I'd like this. In particular, it would allow us to finally revise the data model of pseudo-elements to make some dang sense when combined with other features like relative selectors and nesting, which makes me very happy. The foo::bar syntax will finally no longer be considered a weird compound selector; it's just a legacy way to write the complex selector foo :> bar, with a proper combinator and everything.

Loirooriol commented 2 years ago

If :> is just a normal combinator, then this can address part of #2284 too:

something#very.long :> :is(before, after) /* something#very.long::before, something#very.long::after */

But this wouldn't work:

something#very.long:is(*, * :> after) /* something#very.long, something#very.long::after */
tabatkins commented 2 years ago

Right, it would do the first but not the second. The second is still changing the subject of the selector, which is something that a pseudo-class fundamentally cannot do. We don't allow .foo:is(*, * > *) (to select the foo element and its direct children) either.

romainmenke commented 2 years ago

just a note

:> seems to cause issues in tooling. Quickly tested this in a sass and parcel-css playground and both just give up entirely. PostCSS with popular plugins does a bit better and outputs something but also gives warnings. VSCode colors a lot in red but eventually recovers.

Making : the start of a combinator instead of a pseudo class or pseudo element will require a lot of work. It will take a while for the ecosystem to catch up to something like this :)


Update :

This would be a major improvement imo.

jakearchibald commented 2 years ago

:> seems to cause issues in tooling.

I don't think any new combinator will work out of the box.

romainmenke commented 2 years ago

I don't think any new combinator will work out of the box.

That I understand, but there is a subtle difference between a new combinator which isn't recognised as a combinator and something that causes tools to stop processing CSS.

This is all fixable, but it will take time and considerable effort.


I do think we need the :> operator or something very similar.

jakearchibald commented 2 years ago

Maybe we're saying the same thing, but every potential new combinator I try in https://parcel-css.vercel.app/ fails, except something like ::descendant(foo), and that kind of thing is prohibitively long.

Have you found something that could work better?

romainmenke commented 2 years ago

Have you found something that could work better?

No and I think :> is very expressive and the best choice for the feature.

I've also been looking at the other side in tools -> how many bugs there are because .foo::before is seen as a compound selector and this is quite extensive. 4 of those were created by me in PostCSS plugins (all which have been fixed).

So my initial opinion has changed from "this will be a lot of work" to "this will be a lot of work and a whole class of bugs might get fixed"

Loirooriol commented 2 years ago

Typically, if a complex selector matches an element, you can take just the last compound selector and it will still match.

So if div :> before matches a pseudo-element and :> is a normal combinator, does it mean that a before alone will start matching pseudo-elements? Or are pseudo-elements featureless and can only be matched if there is a ::, :>, :>>? Will before :> marker match a ::before::marker?

tabatkins commented 2 years ago

This cuts into the same issue as when we had the /deep/ combinator proposed for Shadow DOM. Short answer is no, the invariant tightens to being "last compound selector before a tree-hopping combinator", since the initial set of elements visible to selectors are only those in a given tree.

fantasai commented 2 years ago

Suggest using :: instead of :> and ::: instead of :>>.

romainmenke commented 2 years ago

Tab previously looked into :: : https://github.com/w3c/csswg-drafts/issues/2284#issuecomment-364580632

tabatkins commented 2 years ago

Yeah, :: is entirely unusable for reasons we will never be able to get around. That well is poisoned, dug up, then exploded for good measure.

And without ::, ::: doesn't make a lot of sense.

fantasai commented 2 years ago

I'd like to bring up @bradkemper’s response to that old thread here:

I disagree. I don't find it "much more understandable." In fact, I think having "::foo" as one chunk of text (no spaces) makes it much more clear that "foo" is not looking a element in the markup, but is instead something special (an element created by the CSS and then selected).

It might be a little more clear if there is a space before the "::". But honestly, I didn't have any problem parsing "p::before:hover::marker" in my mind. Because I have learned what "::" means already, and learning it with no spaces wasn't hard.

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed [selectors] Child & descendant pseudo element combinators, and agreed to the following:

The full IRC log of that discussion <dael> Topic: [selectors] Child & descendant pseudo element combinators
<dael> github: https://github.com/w3c/csswg-drafts/issues/7346
<dael> TabAtkins: We've had for a little while a minor problem where pseudo elements inside pseudo elements have been tricky to target
<dael> TabAtkins: Repeatedly written incorrect rules that do not correctly target. *::marker doesn't hit inside ::before
<dael> TabAtkins: This will get more problematic over time as we add things like shared element transitions with a family of nested pseudo elements. Targetting a grandchild of a transistion element you have the name the transition tag regardless of where because can't meaningfully use target
<dael> TabAtkins: You can't save effort with nesting either
<Rossen_> q?
<dael> TabAtkins: Jake came up with the idea of having a combinator that can select into pseudo tree as a descdendant combinator. You say a name and you find it regardless of how nested it is
<dael> TabAtkins: Suggested syntax is :> for child pseudo combinator and :>> for the new descendant that goes asdeep as needed
<astearns> 'arrow' being '>', not '->'
<dael> TabAtkins: Specifics is when you use this combinator pseudo elements are elements with tag that is their element. :: is not part of the name
<heycam> q+
<dael> TabAtkins: I think that's the specifics. Allows us to greatly simplify and write code targetting, for example, ::marker
<dael> TabAtkins: Thoughts?
<dael> heycam: Can you summerize why not possible to use regular child and descendants once past first ::? If we're considering things inside top level can we use regular tag name matching?
<fantasai> heycam++
<dael> TabAtkins: Issue with doing that, first is would not let you generically select into psuedo trees. That feels not great that you have to enter the pseudo tree with a specific name and then you can do arbitary.
<Rossen_> ack heycam
<dael> TabAtkins: Slightly more important issue is it presupposes we don't event use child relationships in any other way which I'm not certain we want to guarantee. Some pseudo elements refer to real elements like ::slotted. Right now can't access further into tree but I don't know why we wouldn't do that in the future
<fantasai> See also https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#fragment-scoping
<dael> TabAtkins: instead idea here is there's a separate relationship and this combinator only goes across pseudo/child relationship
<dael> heycam: Makes sense
<dael> fantasai: one specific area we thought about this is ::page or ::colun type things and being able to pick elemental fragments in that fragmented region.
<dael> fantasai: We had thought about doing something where you can style black and white columns differently which needs you to enter the tree
<dael> Rossen_: fantasai I saw your last comment lifting BradK's old response. Is this something we want to repeat here?
<dael> fantasai: I think I was pulling up old comments. Haven't thought deeply to have a conclusion. Idea of going deeper into pseudo tree makes sense. I think bradk comment was we ID psuedo elements with a name that begins ::. The pseudo element is named :: which is how we know it's not real. This prop breaks that naming association
<dael> fantasai: I think it's a comment worth thinking about.
<dael> fantasai: Idea of using combinator syntax I can see benefits to it. I'm trying to think what's a way of keeping these considerations and bring together. Maybe instead of :> we do ::> so maybe still association with pseudo element so at least you've got two :s
<dael> TabAtkins: Using ::>?
<dael> fantasai: Yeah
<dael> TabAtkins: That would be fine. I suggested : to suggest pseudo, but more explict ::> is fine
<vmpstr> q+
<dael> TabAtkins: Obvious solution is :: as combinator and for reasons I explored years ago that's unfortunately impossible due to selector syntax. But it would be fine with ::> and ::>>
<Rossen_> ack vmpstr
<dael> vmpstr: I wanted to ask how would this interact with :has?
<dael> vmpstr: Presumably to know if there is a pseudo element you need up to date style and I don't think we do this now. And with :has you can make it display:none
<dael> TabAtkins: Good question. B/c pseudo elements always exist or conditionally exist I suggest answer is :has can't see pseudo elements when you select that deep. We can address it in the future if there's a need, but I would say we should make it invalid to use the combinator
<dael> fantasai: Should we also make pseudo elements invalid in has in general?
<Rossen_> ч?
<dael> TabAtkins: I don't know
<dael> fantasai: Has the same problem
<Rossen_> q?
<dael> TabAtkins: Not sure you can write selectors that are problematic. If you can we should
<dael> fantasai: I suggest we make both invalid for now
<dael> Rossen_: Additional feedback for TabAtkins or Jake?
<dael> fantasai: I'd like to have a few more people weigh in but this overall makes sense to do this
<dael> TabAtkins: I can start laying down details in spec and we can get eyes once there's text. As long as no obvious objection now I'm happy
<dael> Rossen_: Not hearing any such objection from dozen or so people on the call
<dael> Rossen_: Let's move forward with spec. Once you have that hopefully more people will provide feedback
<dael> fantasai: RElated question, do we want to take a cut of selectors 4 to CR and redraft selectors 5?
<dael> TabAtkins: Think so and happy to put this in selectors 5
<dael> fantasai: Okay. Let's do that and you and I can work on taking a cut of selectors 4
<dael> TabAtkins: sgtm
<dael> Rossen_: Let's resolve on starting selectors 5 with the experimental work from this issue
<dael> Rossen_: Obj?
<dael> RESOLVED: Start selectors 5 with the experimental work from this issue
<dael> Rossen_: For selectors 4, any resolution for that at this point?
<dael> fantasai: We were going to make it invalid to use a pseudo element inside :has
<dael> Rossen_: Is this part of the issue or part of moving selectors 4?
<dael> fantasai: Related to this issue b/c this issue brought what happens if you put pseudo element in :has
<dael> jensimmons: We should open a new issue for that and give it more time
<dael> jensimmons: I don't think we should do it ahead of time b/c we think this is going to go through. We should look at present state
<astearns> +1 to a separate issue to discuss async and bring it back for a resolution
<dael> fantasai: Yes, looking at current and think it's a problem for the same reason as in context of this discussion. I think it's something we didn't consider. Allowing url:has(::marker) is confusion and shouldn't be possible
<fantasai> ol:has(li::marker) is confusing and shouldn't be possible
<dael> jensimmons: Cool. How about open an issue for next week's agenda
<dael> Rossen_: I don't mind that at all since the proposed resolution also didn't come through to me clearly from the previous converation
<dael> Rossen_: Do we want to fork that into a new issue and slot it for next week? It will fit nicely with some other items we're skipping this week
<dael> fantasai: Okay
<dael> Rossen_: Next week we can take all the resolutions to move selectors 4 forward
<dael> Rossen_: fantasai will you open?
<dael> fantasai: Sure
<dael> Rossen_: Anything else on this issue?
<fantasai> https://github.com/w3c/csswg-drafts/issues/7463
Loirooriol commented 2 years ago

Some pseudo elements refer to real elements like ::slotted. Right now can't access further into tree

Actually, the spec allows things like ::shadow > p, https://drafts.csswg.org/selectors-4/#pseudo-element-structure

tabatkins commented 2 years ago

Yeah, but we undefined ::shadow, so no currently-existing pseudo-element allows it, I think. ^_^

ExE-Boss commented 1 year ago

See also https://github.com/w3c/csswg-drafts/issues/5472