w3c / csswg-drafts

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

[css-selectors] Nearest descendant selector #4940

Open radogado opened 4 years ago

radogado commented 4 years ago

Hello, I would like to have a selector for the closest child only, which isn't a direct descendant of the container. It is useful for a carousel with a child (e.g. holding controls) and nested carousels, which shouldn't inherit the styles of the topmost one, while allowing for extra wrappers or supporting cases where we don't fully control the HTML structure.

.wrapper ^ .child to select only the first .child:

<div class="wrapper">
    <div>
        <div class="child"> </div>
    </div>
    <div>
        <div>
            <div class="child"> </div>
        </div>
    </div>
</div>

This has been requested by others before on SO. Thanks.

SelenIT commented 4 years ago

Isn't this already possible to express with complex selectors inside :not() (as Selectors Level 4 allows): .wrapper .child:not(.child .child)?

radogado commented 4 years ago

Isn't this already possible to express with complex selectors inside :not() (as Selectors Level 4 allows): .wrapper .child:not(:child .child)?

What is :child? :not kind of works if we hardcode the number of containers between the top one and the children, but a proper solution would be better. https://codepen.io/radogado/pen/dyYyBWN

Crissov commented 4 years ago

I donʼt fully understand the use case. It sounds like at least one of the following selectors should satisfy the constraints. Perhaps, a further :not(…) would be needed.

radogado commented 4 years ago

I donʼt fully understand the use case. It sounds like at least one of the following selectors should satisfy the constraints. Perhaps, a further :not(…) would be needed.

  • .wrapper > * > .child
  • .wrapper * > .child
  • .wrapper > * .child
  • .wrapper * .child
  • .wrapper > * > .child:first-child
  • .wrapper * > .child:first-child
  • .wrapper > * .child:first-child
  • .wrapper * .child:first-child
  • .wrapper > :first-child > .child
  • .wrapper :first-child > .child
  • .wrapper > :first-child .child
  • .wrapper :first-child .child

It's not about specifying a strict structure like *.wrapper > > .child or the position between siblings (:first-child). Nearest descendant means if a .wrapper has a .child** descendant on level 3 and level 4, we select the nearest level (3). I need it to freely nest any component inside any component without conflicts. Between the following 2 cases,

.wrapper div div .child
.wrapper div div div .child

I need the first one to be selected. Thanks.

SelenIT commented 4 years ago

What is :child?

Oops, just a typo, sorry :( I meant simply .child. Anyway, I also misread you initial example and for some reason assumed that the second .child is nested inside another .child, so my suggestion was incorrect :(

faceless2 commented 4 years ago

It sounds like you're describing the nearest descendent in document order, is that right? So if you had two elements that matched .wrapper > div > .child, only the first one would match your test?

.wrapper > * > :nth-child(1 of .child),
   .wrapper > * > :not(.child) > :nth-child(1 of .child),
   .wrapper > * > :not(.child) > :not(.child) > :nth-child(1 of .child),
   ... and so on, depending on how deep your structure goes.

Not terribly elegant, but better than trying to evaluate

.wrapper > * > :nth-child(1 of .child:not(:has(.child)))

which I think might be equivalent, although I really don't fancy proving that. Also, you probably don't have :has()!

Loirooriol commented 4 years ago

@radogado I think what you want is

.wrapper > .child,
.wrapper:not(:has(> .child)) > * > .child,
.wrapper:not(:has(> .child, > * > .child)) > * > * > .child,
.wrapper:not(:has(> .child, > * > .child, > * > * > .child)) > * > * > * > .child,
/* ... */

i.e. select a 1st level .child, or a 2nd level one if there is no 1st level one, or a 3rd level one if there is no 1st or 2nd level one, and so on.

You need :has() for that, because in order to know if a .child descendant matches, you need to look at all other descendants of .wrapper with a smaller nesting level. But :has() is expensive, so it has only been implemented in print engines.

tabatkins commented 4 years ago

Just because something can be implemented with :has(), doesn't mean it's equivalent to :has(); :has() is a very big hammer and can pound a lot of nails. ^_^

This is just a request for "first descendant matching a selector", which yeah, has been a common request over the years. @radogado's original example is a somewhat novel showing of it, I think (usually it's "I have nested components, and I want to select the nearest one only". Looks like it was last discussed in 2015 on the mailing list.

The simpler form that's just about nesting isn't too bad. .foo /closest/ .bar .baz would be matched as:

  1. Is this element a .baz? If no, bail.
  2. If so, walk the ancestors until I find a .bar. If none, bail.
  3. Continue walking the ancestors, looking for either a .bar or .foo. If I find a .bar, bail. If I find nothing, bail.
  4. Yay, the element matched!

In other words it matches just like a normal descendant combinator, just checking the compound selectors on either side of the combinator, rather than just the one on the left. Slightly more expensive, but not a huge deal. It also might slightly widen the space of mutations to watch for to indicate you need to rerun styling on a subtree.

But the more general form that @radogado opened with where the non-matched element might be in a different subtree than the matched one would be substantially more expensive, and I suspect would kick us out of the realm of possibility.

SebastianZ commented 3 years ago

Something that goes into the same direction and I had several use cases over the years is to match something x levels down in the tree structure and you know the exact number of levels. Could that be covered by this, too?

Using @tabatkins' syntax that could be done by .foo /x/ .bar where x is the number of levels the right side of the selector is separated from the left part.

So for example, .wrapper /3/ .child applied to @radogado's structure

<div class="wrapper">
    <div>
        <div class="child"> </div>
    </div>
    <div>
        <div>
            <div class="child"> </div>
        </div>
    </div>
</div>

would match the latter <div class="child"> because it's three levels down.

Sebastian

SelenIT commented 3 years ago

Isn't .wrapper /3/ .child basically the same as .wrapper > * > * > .child?

radogado commented 3 years ago

Thanks for the comments. A lot of people assumed I know the exact structure, which was never the idea and would be easily solvable. Here is a rephrasing of the HTML, where the ellipsis represent unknown nested levels of elements.

<div class="wrapper">
    ...
        <div class="child"> </div>
    ...
    <div>
       ...
            <div class="child"> </div>
        ...
    </div>
</div>
SebastianZ commented 3 years ago

Isn't .wrapper /3/ .child basically the same as .wrapper > * > * > .child?

Yes, though the deeper the child is placed the longer is the chain of > *, which quickly becomes unreadable.

Sebastian