w3c / csswg-drafts

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

[css-selectors-4] Selector for element with another element as ancestor #9130

Closed dpk closed 1 year ago

dpk commented 1 year ago

Rough proposal: a pseudo-class, such as p:within(a) selects p elements which have an a element anywhere in their ancestor tree.

E.g. it would select any of the <p> elements here:

<div>
  <a href="http://example.org/">
    <p>I match
  </a>
</div>

<a href="http://example.org/">
  <div>
    <p>I match
  </div>
</a>

But not this one:

<div>
  <p>I don’t match, <a href="http://example.org/">me neither</a>
</div>

While writing my first major design using CSS Nesting, this seemed like a missing feature, mainly for maintainability but also because truly accounting for all possible nesting situations to target the element wanted using regular selectors could be tricky within a complex nesting of selectors. A strong use case in particular (as implied above) is when elements might sometimes be inside links and sometimes not. This would allow them to be styled or restyled in a way more clearly indicating that they can be interacted with as links.

Link to spec: https://www.w3.org/TR/selectors-4/

rthrejheytjyrtj545 commented 1 year ago

almost like that?

/*/ use cases in post /*/
A P
/*/ description in post /*/
:is(A /*/ for A:root case /*/, :has(A)) P:not(:has(A))
Loirooriol commented 1 year ago

Yeah I don't get the difference between p:within(a) and a p. See https://drafts.csswg.org/selectors-4/#descendant-combinators

If you just want to impose extra conditions, like foo p:within(a), then use foo p:is(a *)

tabatkins commented 1 year ago

Yup, a p is what you want, and you can use :is() as @Loirooriol said if you need to impose multiple ancestor conditions that aren't related to each other.

Note tho that your first example:

<div>
  <a href="http://example.org/">
    <p>I match
  </a>
</div>

won't match the selector because, due to the way HTML parsing is defined, inline elements are auto-closed by a p and then reopened inside of them. That is, the DOM structure produced by that markup is:

DIV
├ A href="http://example.org/"
└ P
  └ A href="http://example.org/"
    └#text: I match

(Your second example works as intended, because div does not have that special behavior. So it is able to nest inside the a without a problem, and then the p sees that it has a div parent and acts normally as well. HTML parsing has a lot of funky quirks that it's developed over the decades that HTML has been in use.)

This might be the source of your confusion!

Loirooriol commented 1 year ago

Actually, the HTML parser can place both p and div inside an a, but this doesn't happen if the end tag is missing like in the example. I think this is handled in https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm. So this works:

<!DOCTYPE html>
<style>a p { border: solid green }</style>
<div>
  <a href="http://example.org/">
    <p>I match</p>
  </a>
</div>
dpk commented 1 year ago

Sorry, I think I didn’t explain the motivation for this. Obviously a p will match this in the general case, but I mean this as a convenience/maintainability solution in quite highly nested cases like this:

.aleph {
  /* ... styles for .aleph ... */
  & .beth {
     /* ... styles for .aleph .beth ... */
    & .gimel {
      /* ... styles for .aleph .beth .gimel ... */
      & .daleth {
        /* ... styles for .aleph .beth .gimel .daleth ... */
        &:within(a) {
        /* special styles for .daleth when within a link, regardless of where the a
           is in the tree of .aleph .beth .gimel */
        } } } } }

Afaict the only equivalent of this using the whitespace selector would be to write out the combinations by hand, and exiting the entire tree of selectors where the rest of the styles for .daleth are:

a .aleph .beth .gimel .daleth,
.aleph a .beth .gimel .daleth,
.aleph .beth a .gimel .daleth,
.aleph .beth .gimel a .daleth {
  /* special styles for .daleth within a link */
}

This is long-winded and gets even more complicated if one imagines situations such as .gewa:within(.bercna:within(.aza)) with a tree of selectors preceding it. The need to exit the tree of nested selectors is also bad for maintainability because it means all properties relevant to a particular style are no longer together in the stylesheet source.

I also don’t see how :is can be used to solve this, but maybe I’m missing something.

Loirooriol commented 1 year ago

Then you should use

.aleph {
  /* ... styles for .aleph ... */
  & .beth {
     /* ... styles for .aleph .beth ... */
    & .gimel {
      /* ... styles for .aleph .beth .gimel ... */
      & .daleth {
        /* ... styles for .aleph .beth .gimel .daleth ... */
        a & { /* or `&:is(a *)` works too */
        /* special styles for .daleth when within a link, regardless of where the a
           is in the tree of .aleph .beth .gimel */
        } } } } }

Note that a & only requires a to be an ancestor of the element matched by the parent selector, basically it will behave like a :is(.aleph .beth .gimel .daleth).

Unlike a .aleph .beth .gimel .daleth, a & doesn't require a to be an ancestor of .aleph.

dpk commented 1 year ago

a & doesn’t work because it violates the requirement that nested selectors have to start with a punctuation symbol, but &:is(a *) does! Thanks!

Loirooriol commented 1 year ago

AFAIK that requirement was dropped in 02db7b2e364bd3184e23ebb7f475e6b4347d5406

dpk commented 1 year ago

Good to know! But it’s still present in browsers (and, afaict, in the PostCSS plugin I’m using as a stopgap to support Firefox) so for now I still need to follow it

SebastianZ commented 1 year ago

and, afaict, in the PostCSS plugin I’m using as a stopgap to support Firefox

As a side note, Firefox 117 will ship with nesting and without the punctuation symbol requirement.

Sebastian

tabatkins commented 1 year ago

Yup, that restriction got dropped a while back and implementations will follow. We can't fix legacy implementations by adding new features, as by definition they're not being updated. ^_^