Skip to content

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

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

Closed
dpk opened this issue Jul 31, 2023 · 11 comments
Closed

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

dpk opened this issue Jul 31, 2023 · 11 comments
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. selectors-4 Current Work

Comments

@dpk
Copy link

dpk commented Jul 31, 2023

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/

@bleper
Copy link

bleper commented Aug 1, 2023

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
Copy link
Contributor

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
Copy link
Member

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!

@tabatkins tabatkins added selectors-4 Current Work Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. labels Aug 1, 2023
@Loirooriol
Copy link
Contributor

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
Copy link
Author

dpk commented Aug 2, 2023

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
Copy link
Contributor

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
Copy link
Author

dpk commented Aug 2, 2023

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
Copy link
Contributor

AFAIK that requirement was dropped in 02db7b2

@dpk
Copy link
Author

dpk commented Aug 2, 2023

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
Copy link
Contributor

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
Copy link
Member

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. ^_^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. selectors-4 Current Work
Projects
None yet
Development

No branches or pull requests

5 participants