w3c / csswg-drafts

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

[css-selectors] has-child selector #4903

Closed chrishtr closed 1 year ago

chrishtr commented 4 years ago

Proposal: add a selector that matches an element if a direct child matches the selector in the function.

"all a elements that contain an img" -> a:has-child(img)

Motivation: #2 missing-features request in the state-of-CSS 2019 survey.

Since it only matches direct children, it may be no more expensive than sibling selectors.

A related selector that was considered and removed as too expensive and slow (AIUI) is :has. See also resolution regarding it here.

@tabatkins @lilles

andruud commented 4 years ago

Hopefully most of the respondents actually did mean parent (:has-child) and not ancestor (:has-descendant).

@argyleink @una Do you have any gut feeling regarding this? The survey says "parent selector", which we're interpreting as "immediate parent selector", whereas I'm worried some respondents actually want "ancestor selector" (which would be way more expensive).

lilles commented 4 years ago

Regarding performance cost, it's more similar to indirect adjacent or :nth-* selectors than direct sibling selectors. In Blink, invalidation sounds straightforward, quite similar to how we invalidate for the previously mentioned selectors.

:nth-selectors are typically implemented with a cache (at least Blink and Gecko/Servo) to avoid repeated O(n) traversals for multiple siblings. :has-child() would only be done once for a set of siblings, since they can only have a single parent, but if you have multiple :has-child() selectors matching the same element, we would possibly want to avoid multiple passes over the children somehow for :has-child().

The current spec text for :has() does not limit this to the rightmost compound. It's not obvious to me that: "div a:has-child(img) span {}" does not have more performance implications than only allowing :has-child() in the rightmost compound.

emilio commented 4 years ago

Hmm, I don't think invalidation sounds so straight-forward? Now dom mutations can invalidate styles of ancestors, which as far as I know all engines (including Blink) assume cannot happen.

Sure, it's only one-level ancestor, but it is not if you nest them, right? So div:has-child(div:has-child(img)) or such.

emilio commented 4 years ago

And at that point you may as well just implement full :has support?

lilles commented 4 years ago

Yes, div:has-child(div:has-child(img)) is essentially a different syntax for a variant of :has, so my assumption would be that :has-child() only takes a compound or compound list, not allowing nested has-child() or any other simple selectors which can contain combinators sigh, and as previously mentioned only in the rightmost compound.

In Blink, the invalidation for nth-* selectors is similar to what we would do here. Mark an element if it either matched or failed to match a :has-child() selector, and when any of its children mutates and the flag is set, mark that parent for style recalc.

bkardell commented 4 years ago

Just want to agree with @andruud here that I am a little unconvinced that these are not confused in the poll and overlapping. I asked this back in Feb, connected by them via twitter on just how it was even worded? but didn't get any reply. In most of my conversations, by parent selector, many people seem to have different interpretations

I'm just one person tho, and understand that's all anecdotal.

It's also not to say that something very limited has no use... It feels worth a nice post explaining a proposal and its limits and getting feedback from devs though first?

chrishtr commented 4 years ago

@bkardell I agree that we don't have enough data to say whether parent means parent or ancestor, or similar potential abiguities.

A post explaining and gathering feedback would be excellent. Were you volunteering to help with that? Would be great if so, just checking.

tabatkins commented 4 years ago

Sure, it's only one-level ancestor, but it is not if you nest them, right? So div:has-child(div:has-child(img)) or such.

In previous discussions of this (largely predating us using GH, unfortunately), it was presumed that nesting :has-child() would be invalid.

So you could have :has-child() at multiple points in a selector, like div:has-child(img) + label:has-child(:checked), but you wouldn't ever be able to recurse with it.

I assume we'd definitely impose this restriction, else this is just a less convenient way of writing :has(), as you say.

lilles commented 4 years ago

Thinking about it again, limiting this to rightmost compound would not be a requirement. Tab's "div:has-child(img) + label:has-child(:checked)" example, or "div:has-child(img) span" should also have similar performance implications.

argyleink commented 4 years ago

Hopefully most of the respondents actually did mean parent (:has-child) and not ancestor (:has-descendant). @argyleink @una Do you have any gut feeling regarding this?

From my experience folks exclusively mean ancestor querying. Eg, they want a child to own how it responds to a class or state findable further up the DOM. @bkardell had it right when using the < character, as a way of visualizing the intent .dark < .my-component {...}. I dont know anyone who's aware of :has when talking about this topic.

bkardell commented 4 years ago

@chrishtr I would be very happy to do that, yes, if we can agree to some concrete proposal(s) to offer and explain that also describe the relatively likeliness of implement-ability. It's probably one of those things that even if it is a kind of unofficial poll though should probably have some review to make sure it doesn't create a similar confusion indirectly somehow so maybe if another person or two would offer to work on it together, that would be most helpful.

SebastianZ commented 4 years ago

The :has-child() selector is effectively a sub-feature of :has() and it sounds like allowing arbitrarily deep selectors may not be implementable in a performant way.

So another approach to this would be to restrict :has() to a simple relative selector, i.e. <combinator> <simple-selector>. This avoids introducing two functional overlapping pseudo-classes, already covers many use cases, and is more performant than allowing complex selectors.

And if that resolves the performance issues regarding :has(), it hopefully gains some traction by implementors and doesn't require the note that "Supporting the :has() pseudo-class is not required to conform to this specification.".

In a later level, :has() may then still be enhanced to allow complex selectors to cover the rest of the use cases, but in the meantime, authors already get something they can work with.

Sebastian

faceless2 commented 4 years ago

PrinceXML has already implemented :has() as currently specified and is shipping, and we have too (but are't shiipping yet).

tabatkins commented 4 years ago

PrinceXML has already implemented :has() as currently specified and is shipping, and we have too (but are't shiipping yet).

Yes, printing implementations can implement :has() reasonably; the problem with :has() is that it means the style of an element can be affected by the qualities of any element on the entire page, and so progressive rendering and efficient updates become vastly more difficult and/or expensive to do.

If your implementation isn't bound by web reality in having to get correct pixels on screen during loading and after each mutation absolutely ASAP, then the downsides aren't killer, and :has() is a nice useful feature.

[Sebastian's suggestions]

No simplification of :has() that still allows arbitrary combinators is likely to have a noticeable effect on the perf. It's not the cost of evaluating the selector that's killer, it's the number of elements that have to be re-evaluated on every mutation. Currently it's all the descendants of the mutated element's parent; they're the only elements that can observe the mutation with today's selectors. :has() makes the entire document potentially able to observe the mutation.

:has-child() widens the set of possibly-reacting elements to all the descendants of the mutated element's grandparent, which can be a considerably larger set, but is still much less than the entire document.

From my experience folks exclusively mean ancestor querying. Eg, they want a child to own how it responds to a class or state findable further up the DOM. @bkardell had it right when using the < character, as a way of visualizing the intent .dark < .my-component {...}.

I'm not sure what you mean here - you seem to be talking about normal descendant combinators.

bkardell commented 4 years ago

From my experience folks exclusively mean ancestor querying. Eg, they want a child to own how it responds to a class or state findable further up the DOM. @bkardell had it right when using the < character, as a way of visualizing the intent .dark < .my-component {...}.

I'm not sure what you mean here - you seem to be talking about normal descendant combinators.

I'm sure this was just a misread, > is a normal child combinator. This (<) would be a parent combinator?

faceless2 commented 4 years ago

@tabatkins, I know it's very expensive and unlikely be be implemented in browsers. My comment was a reply to @SebastianZ - I'd suggest drastically changing the meaning of the has() function when you already have shipping implementations isn't a great idea.

tabatkins commented 4 years ago

I'm sure this was just a misread,

Right, ignoring what syntax Adam used, his description sounds like he's just talking about descendant combinators. Elements react to their ancestors today.

bkardell commented 4 years ago

@tabatkins I'm sorry, I'm not sure what you are clarifying here so I'm not sure if I need to clarify in turn. Apologies if I am repeating or clarifying something obvious, but ...

.dark < .my-component in the example is suggesting that .dark (the parent of .my-component) is the subject, not .my-component... is that clear? Further some seem to intuit that this would just "change the directionality" such that space (descendant) becomes ancestor.. So body .my-component < .x would make body the subject. To be 100% clear, I am simply recounting the many asks/explanations I have heard with the hope of shedding light on the cited data from the poll results mentioned and not suggesting these as solutions.

tabatkins commented 4 years ago

Right, I get that's what .dark < .my-component would mean.

But Adam's comment said:

Eg, they want a child to own how it responds to a class or state findable further up the DOM.

That is just standard selectors: .dark .my-component means the .my-component is owning how it responds to a class (.dark) further up in the DOM.

Further some seem to intuit that this would just "change the directionality" such that space (descendant) becomes ancestor. So body .my-component < .x would make body the subject.

Ooooof, no, this would be real bad: contextual re-interpretation of the entire selector based on a combinator that might not show up until the very end! That's a "garden-path" issue, where your initial read seems to lead you down one direction, only for something at the end to make you realize you were going the wrong way the whole time, requiring you to back up and re-interpret the entire thing. Not to mention it makes selectors like foo > bar < baz either non-sensical or, at best, dramatically more confusing. So no, definitely not going that way.

SebastianZ commented 4 years ago

@tabatkins, I know it's very expensive and unlikely be be implemented in browsers. My comment was a reply to @SebastianZ - I'd suggest drastically changing the meaning of the has() function when you already have shipping implementations isn't a great idea.

The syntax I suggested is meant to be a subset of the currently specified one, and by that should not be a breaking change. Implementations already covering the currently defined syntax allowing complex selectors are then just ahead of time.

Another option would be to already define both syntaxes in Level 4 and add a hint that implementations should support the fully-featured syntax if performance is irrelevant (e.g. in print engines) and fall back to the simpler syntax if performance is critical (like in browser engines).

No simplification of :has() that still allows arbitrary combinators is likely to have a noticeable effect on the perf. It's not the cost of evaluating the selector that's killer, it's the number of elements that have to be re-evaluated on every mutation. Currently it's all the descendants of the mutated element's parent; they're the only elements that can observe the mutation with today's selectors. :has() makes the entire document potentially able to observe the mutation.

The suggested syntax with a single <combinator> restricts the number of elements to either children (> combinator), next siblings (+ combinator), subsequent siblings (~ combinator), or a column relation (|| combinator). With that, the performance impact should be very much mitigated.

Sebastian

una commented 4 years ago

I just wanted to throw my 2 cents in here -- I've used the :not(:empty) pattern a few times, so I feel like a :has-child is generally a good idea. I do agree that it would probably be better to specify that child with :has though as a better specification means.

In that case, how would :has-child provide something that :not(:empty) and :has do not?

tabatkins commented 4 years ago

In that case, how would :has-child provide something that :not(:empty) and :has do not?

:has-child() has a chance of being implementable, which :has() lacks. ^_^

SebastianZ commented 4 years ago

In that case, how would :has-child provide something that :not(:empty) and :has do not?

:has-child() as defined in the original post allows to specify the child, which :not(:empty) does not.

Sebastian

una commented 4 years ago

Regarding the direct child vs. ancestor conversation, I think we agree that most people generally want "ancestor", as deeply nested components are common (and becoming increasingly common with framework-based UI systems).

IMG_0072

From a developer experience point of view, :has is far superior (A). Upon implementing :has-child(B), we'll likely see a lot of chaining. The example above is simplified, as we're only going "two levels up". This could get much more convoluted, especially if folks are going up and down the tree to apply styles.

That being said, I'd rather have something than nothing.

Loirooriol commented 4 years ago

@una Note that if you use multiple :has-child() chained one after the other, you will just impose additional constraints to the same element. So

ancestor:has-child(.container):has-child(.celebrate) {}

should behave like

ancestor:has-child(.container.celebrate, .container ~ .celebrate, .celebrate ~ .container) {}

In order to go "two levels up" from .container > .celebrate, you would need nesting:

ancestor:has-child(.container:has-child(.celebrate)) {}

which some comments above assumed wouldn't be allowed.

chrishtr commented 4 years ago

That being said, I'd rather have something than nothing.

@una but would developers be able to use it effectively? That is hard to gauge. Any additional data on whether this would be helpful in practice would help to prioritize this feature.

SelenIT commented 4 years ago

I'd guess that any native CSS solution would be arguably more effective than most JS-based workarounds that developers still have to use instead of it...

songmelted commented 3 years ago

I am an infrequent user of css. However, today, I came across the need to select all ancestors of an element.

It would have been nice to have had a css selector to identify all ancestors of a given element to use in conjunction with javascript's querySelectorAll()

It is easy to find all the descendants of an element using *. It feels like it should be as easy to find all parents.

As is, I had to use something like this

https://gomakethings.com/how-to-get-all-parent-elements-with-vanilla-javascript/

or like this

https://stackoverflow.com/questions/8729193/how-to-get-all-parent-nodes-of-given-element-in-pure-javascript

bramus commented 2 years ago

If I'm not overlooking things, this issue may be closed now that :has() has landed after all?

With it, we can use a:has(> img) to target “all a elements that contain an img” as per OPs request.

frivoal commented 2 years ago

:has() isn't there in all browsers yet, nor is it certain that it will be. But it would be great if it did, and if so, yes, this issue indeed becomes redundant.

fantasai commented 1 year ago

@chrishtr I think we can close this out in favor of :has() now? :) https://caniuse.com/css-has

chrishtr commented 1 year ago

\o/