Skip to content

[selectors] The forgiving nature of :has breaks jQuery when used with a complex :has selector #7676

Closed

Description

@mgol

The fact that the native :has pseudo-class:
https://drafts.csswg.org/selectors/#relational
takes forgiving-relative-selector-list as an argument:
https://drafts.csswg.org/selectors/#forgiving-selector
means the contents are not validated.

jQuery has supported the :has pseudo-class for ages. However, its support is more powerful; in particular, it allows for jQuery extensions like :contains to appear within :has.

The way the jQuery selector engine works (and have worked for a long time), the selector is tried against querySelectorAll with minor modifications and if it throws, it goes through the internal selector engine. It's either-or.

Since selectors like ul:has(li:contains('Item')) no longer throw in Chrome, jQuery defers to querySelectorAll and such a selector returns 0 results even if the jQuery path should match something.

We can of course try to patch it in jQuery, perhaps defaulting selectors containing :has to use the jQuery selector engine. But it won't help countless apps using older jQuery versions. So I wanted to bring it for consideration for the standards committee. I understand if the decision is going to be "no changes in the spec are planned" but it'd be good to have this discussion and the decision made knowing the consequences.

As you can see, it didn't take long after the Chrome 105 release for us to get a bug report about this breakage:
jquery/jquery#5098
Implementing :has according to the spec makes the browser break the jQuery test suite right now.

Activity

  1. mgol commented on Sep 2, 2022

    @mgol
    Author

    It's worth noting this affects all jQuery versions from 1.3 released in January, 2009 to the current 3.6.1 released a few days ago.

  2. lilles commented on Sep 2, 2022

    @lilles
    Member

    This did not break with Safari shipping, because WebKit did not implement using forgiving selector list per spec: https://bugs.webkit.org/show_bug.cgi?id=244708

  3. lilles commented on Sep 2, 2022

    @lilles
    Member
  4. byung-woo commented on Sep 2, 2022

    @byung-woo
    Member

    Even if we make the :has() unforgiving, it seems that there can be still same issues in :has(:is(.a:has(.b), .c)) since :is() is forgiving. (It seems that @lilles already asked about this here : jquery/jquery#5098 (comment))

  5. anttijk commented on Sep 2, 2022

    @anttijk

    The current WebKit behavior (not by design) is to use forgiving parsing but fail the selector if the selector list ends up empty. That is

    :has(:foo, bar) { }

    is valid and parses to

    :has(bar) { }

    but

    :has(:foo) is invalid.

    Perhaps this is actually the best option considering the jQuery issue.

  6. emilio commented on Sep 2, 2022

    @emilio
    Collaborator

    The way the jQuery selector engine works (and have worked for a long time), the selector is tried against querySelectorAll with minor modifications and if it fails, it goes through the internal selector engine. It's either-or.

    Shouldn't jQuery check CSS.supports("selector(:has(....))"), which should be unforgiving per spec? See #7280

  7. byung-woo commented on Sep 2, 2022

    @byung-woo
    Member

    Shouldn't jQuery check CSS.supports("selector(:has(....))"), which should be unforgiving per spec? See #7280

    I agree that this is the correct way to validate :has() usage, but it's sad that even if JQuery applies this to the new version, there may be unexpected regressions when someone uses an older JQuery version. :(

    Now I'm trying to merge a fix to Chrome to avoid existing JQuery conflict by following the WebKit's behavior. But I think that we need a way to handle this situation properly.

    The one way I can imagine is to rename the pseudo class name in the spec so that there will not be any conflict with existing JQuery implementation (:has() -> :some-other-good-name())

  8. emilio commented on Sep 2, 2022

    @emilio
    Collaborator

    Is there any real-world site broken by the change? How frequent do we expect it to be to use jQuery-specific selectors inside :has() to begin with?

  9. emilio commented on Sep 2, 2022

    @emilio
    Collaborator

    Also, is this a "recent version of jQuery" kinda thing? Or would this change cause issues on older jQuery versions?

  10. emilio commented on Sep 2, 2022

    @emilio
    Collaborator

    If there's no real-world breakage, it might be worth just getting jQuery fixed by using CSS.supports(), rather than introduce a weird quirk in how :has() works vs. other modern selectors...

  11. mgol commented on Sep 2, 2022

    @mgol
    Author

    @emilio

    Shouldn't jQuery check CSS.supports("selector(:has(....))"), which should be unforgiving per spec? See #7280

    In Chrome 105, CSS.supports('selector(:has(div:contains(div)))') returns true. I'd gladly use this in jQuery if this was working as advertised.

    Also, is this a "recent version of jQuery" kinda thing? Or would this change cause issues on older jQuery versions?

    As I wrote in #7676 (comment), this affects virtually all jQuery versions used in the wild.

    Is there any real-world site broken by the change? How frequent do we expect it to be to use jQuery-specific selectors inside :has() to begin with?

    I am unable to answer this question. But it did both break our test suite and get us a report from a user at jquery/jquery#5098. @Rinzwind did you encounter this issue just in manual testing or did it actually break anything for you on a live site?

  12. mgol commented on Sep 2, 2022

    @mgol
    Author

    @byung-woo

    Now I'm trying to merge a fix to Chrome to avoid existing JQuery conflict by following the WebKit's behavior. But I think that we need a way to handle this situation properly.

    Note that the WebKit behavior still breaks some jQuery selectors. True, it doesn't break:

    div:has(span:contains('Item'))
    

    but it still breaks:

    div:has(div, span:contains('Item'))
    

    Maybe that latter kind of selectors is not used as often in the wild.

  13. lilles commented on Sep 2, 2022

    @lilles
    Member
  14. 77 remaining items

  15. mgol commented on Feb 8, 2023

    @mgol
    Author

    I have some doubts about the generic recommendation of wrapping with :is() or :where() to get forgiveness; I submitted an issue about this: #8430.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions