From 848de625425c6b08ec9d8ad9a4bcab7e913c2477 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Mon, 19 Dec 2022 18:43:30 +0100 Subject: [PATCH] Selector: Make selector lists work with `qSA` again jQuery 3.6.2 started using `CSS.supports( "selector(SELECTOR)" )` before using `querySelectorAll` on the selector. This was to solve gh-5098 - some selectors, like `:has()`, now had their parameters parsed in a forgiving way, meaning that `:has(:fakepseudo)` no longer throws but just returns 0 results, breaking that jQuery mechanism. A recent spec change made `CSS.supports( "selector(SELECTOR)" )` always use non-forgiving parsing, allowing us to use this API for what we've used `try-catch` before. To solve the issue on the spec side for older jQuery versions, `:has()` parameters are no longer using forgiving parsing in the latest spec update but our new mechanism is more future-proof anyway. However, the jQuery implementation has a bug - in `CSS.supports( "selector(SELECTOR)" )`, `SELECTOR` needs to be a `` and not a ``. Which means that selector lists now skip `qSA` and go to the jQuery custom traversal: ```js CSS.supports("selector(div:valid, span)"); // false CSS.supports("selector(div:valid)"); // true CSS.supports("selector(span)"); // true ``` To solve this, this commit wraps the selector list passed to `CSS.supports( "selector(:is(SELECTOR))" )` with `:is`, making it a single selector again. See: * https://w3c.github.io/csswg-drafts/css-conditional-4/#at-supports-ext * https://w3c.github.io/csswg-drafts/selectors-4/#typedef-complex-selector * https://w3c.github.io/csswg-drafts/selectors-4/#typedef-complex-selector-list Fixes gh-5177 Closes gh-5178 Ref w3c/csswg-drafts#7280 (cherry picked from commit 09d988b774e7ff4acfb69c0cde2dab373559aaca) --- src/selector.js | 20 ++++++++++++++------ test/unit/selector.js | 22 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/selector.js b/src/selector.js index eae760c58..47e4cbe14 100644 --- a/src/selector.js +++ b/src/selector.js @@ -306,15 +306,20 @@ function find( selector, context, results, seed ) { // `qSA` may not throw for unrecognized parts using forgiving parsing: // https://drafts.csswg.org/selectors/#forgiving-selector - // like the `:has()` pseudo-class: - // https://drafts.csswg.org/selectors/#relational + // like the `:is()` pseudo-class: + // https://drafts.csswg.org/selectors/#matches // `CSS.supports` is still expected to return `false` then: // https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn // https://drafts.csswg.org/css-conditional-4/#dfn-support-selector if ( support.cssSupportsSelector && + // `CSS.supports( "selector(...)" )` requires the argument to the + // `selector` function to be a ``, not + // a `` which our selector may be. Wrapping with + // `:is` works around the issue and is supported by all browsers + // we support except for IE which will fail the support test anyway. // eslint-disable-next-line no-undef - !CSS.supports( "selector(" + newSelector + ")" ) ) { + !CSS.supports( "selector(:is(" + newSelector + "))" ) ) { // Support: IE 9 - 11+ // Throw to get to the same code path as an error directly in qSA. @@ -570,6 +575,10 @@ function setDocument( node ) { return document.querySelectorAll( ":scope" ); } ); + // Support: IE 9 - 11+ + // IE doesn't support `CSS.supports( "selector(...)" )`; it will throw + // in this support test. + // // Support: Chrome 105+, Firefox <106, Safari 15.4+ // Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`. // @@ -749,9 +758,8 @@ function setDocument( node ) { // `:has()` uses a forgiving selector list as an argument so our regular // `try-catch` mechanism fails to catch `:has()` with arguments not supported // natively like `:has(:contains("Foo"))`. Where supported & spec-compliant, - // we now use `CSS.supports("selector(SELECTOR_TO_BE_TESTED)")` but outside - // that, let's mark `:has` as buggy to always use jQuery traversal for - // `:has()`. + // we now use `CSS.supports("selector(:is(SELECTOR_TO_BE_TESTED))")`, but + // outside that we mark `:has` as buggy. rbuggyQSA.push( ":has" ); } diff --git a/test/unit/selector.js b/test/unit/selector.js index 211ef6d90..874692eaa 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -415,7 +415,7 @@ QUnit.test( "name", function( assert ) { } ); QUnit.test( "comma-separated", function( assert ) { - assert.expect( 4 ); + assert.expect( 10 ); var fixture = jQuery( "

" ); @@ -423,6 +423,26 @@ QUnit.test( "comma-separated", function( assert ) { assert.equal( fixture.find( "h2, div p" ).filter( "h2" ).length, 1, "has to find one

" ); assert.equal( fixture.find( "h2 , div p" ).filter( "p" ).length, 2, "has to find two

" ); assert.equal( fixture.find( "h2 , div p" ).filter( "h2" ).length, 1, "has to find one

" ); + assert.equal( fixture.find( "h2 ,div p" ).filter( "p" ).length, 2, "has to find two

" ); + assert.equal( fixture.find( "h2 ,div p" ).filter( "h2" ).length, 1, "has to find one

" ); + assert.equal( fixture.find( "h2,div p" ).filter( "p" ).length, 2, "has to find two

" ); + assert.equal( fixture.find( "h2,div p" ).filter( "h2" ).length, 1, "has to find one

" ); + assert.equal( fixture.find( "h2\t,\rdiv p" ).filter( "p" ).length, 2, "has to find two

" ); + assert.equal( fixture.find( "h2\t,\rdiv p" ).filter( "h2" ).length, 1, "has to find one

" ); +} ); + +QUnit.test( "comma-separated, only supported natively (gh-5177)", function( assert ) { + assert.expect( 5 ); + + var fixture = jQuery( "
" ); + + fixture.appendTo( "#qunit-fixture" ); + + assert.equal( fixture.find( "input:valid, span" ).length, 2, "has to find two elements" ); + assert.equal( fixture.find( "input:valid , span" ).length, 2, "has to find two elements" ); + assert.equal( fixture.find( "input:valid ,span" ).length, 2, "has to find two elements" ); + assert.equal( fixture.find( "input:valid,span" ).length, 2, "has to find two elements" ); + assert.equal( fixture.find( "input:valid\t,\rspan" ).length, 2, "has to find two elements" ); } ); QUnit.test( "child and adjacent", function( assert ) { -- 2.39.5