Skip to content
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

Selector: Use jQuery :has if CSS.supports(selector(...)) non-compliant #5107

Merged
merged 1 commit into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import whitespace from "./var/whitespace.js";
import rbuggyQSA from "./selector/rbuggyQSA.js";
import rtrim from "./var/rtrim.js";
import isIE from "./var/isIE.js";
import support from "./selector/support.js";

// The following utils are attached directly to the jQuery object.
import "./selector/contains.js";
Expand Down Expand Up @@ -252,6 +253,27 @@ function find( selector, context, results, seed ) {
}

try {

// `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
// `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 &&

// eslint-disable-next-line no-undef
!CSS.supports( "selector(" + newSelector + ")" ) ) {

// Support: IE 11+
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually not just targeted at IE but at all browsers where CSS.supports is buggy. I should update the comment.

// Throw to get to the same code path as an error directly in qSA.
// Note: once we only support browser supporting
// `CSS.supports('selector(...)')`, we can most likely drop
// the `try-catch`. IE doesn't implement the API.
throw new Error();
}

push.apply( results,
newContext.querySelectorAll( newSelector )
);
Expand Down
41 changes: 30 additions & 11 deletions src/selector/rbuggyQSA.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import isIE from "../var/isIE.js";
import whitespace from "../var/whitespace.js";
import support from "./support.js";

var rbuggyQSA = isIE && new RegExp(
var rbuggyQSA = [];

// Support: IE 9 - 11+
// IE's :disabled selector does not pick up the children of disabled fieldsets
":enabled|:disabled|" +
if ( isIE ) {
rbuggyQSA.push(

// Support: IE 11+
// IE 11 doesn't find elements on a `[name='']` query in some cases.
// Adding a temporary attribute to the document before the selection works
// around the issue.
"\\[" + whitespace + "*name" + whitespace + "*=" +
whitespace + "*(?:''|\"\")"
// Support: IE 9 - 11+
// IE's :disabled selector does not pick up the children of disabled fieldsets
":enabled",
":disabled",

);
// Support: IE 11+
// IE 11 doesn't find elements on a `[name='']` query in some cases.
// Adding a temporary attribute to the document before the selection works
// around the issue.
"\\[" + whitespace + "*name" + whitespace + "*=" +
whitespace + "*(?:''|\"\")"
);
}

if ( !support.cssSupportsSelector ) {

// Support: Chrome 105+, Safari 15.4+
// `: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()`.
rbuggyQSA.push( ":has" );
}

rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );

export default rbuggyQSA;
24 changes: 24 additions & 0 deletions src/selector/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import support from "../var/support.js";

try {
/* eslint-disable no-undef */

// Support: Chrome 105+, Firefox 104+, Safari 15.4+
// Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`.
//
// `:is()` uses a forgiving selector list as an argument and is widely
// implemented, so it's a good one to test against.
support.cssSupportsSelector = CSS.supports( "selector(*)" ) &&

// `*` is needed as Safari & newer Chrome implemented something in between
// for `:has()` - it throws in `qSA` if it only contains an unsupported
// argument but multiple ones, one of which is supported, are fine.
// We want to play safe in case `:is()` gets the same treatment.
!CSS.supports( "selector(:is(*,:jqfake))" );

/* eslint-enable */
} catch ( e ) {
support.cssSupportsSelector = false;
}

export default support;
12 changes: 11 additions & 1 deletion test/unit/selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -931,13 +931,23 @@ QUnit.test( "pseudo - nth-last-of-type", function( assert ) {
} );

QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "pseudo - has", function( assert ) {
assert.expect( 3 );
assert.expect( 4 );

assert.t( "Basic test", "p:has(a)", [ "firstp", "ap", "en", "sap" ] );
assert.t( "Basic test (irrelevant whitespace)", "p:has( a )", [ "firstp", "ap", "en", "sap" ] );
assert.t( "Nested with overlapping candidates",
"#qunit-fixture div:has(div:has(div:not([id])))",
[ "moretests", "t2037", "fx-test-group", "fx-queue" ] );

// Support: Safari 15.4+, Chrome 105+
// `qSA` in Safari/Chrome throws for `:has()` with only unsupported arguments
// but if you add a supported arg to the list, it will run and just potentially
// return no results. Make sure this is accounted for. (gh-5098)
// Note: Chrome 105 has this behavior only in 105.0.5195.125 or newer;
// initially it shipped with a fully forgiving parsing in `:has()`.
assert.t( "Nested with list arguments",
"#qunit-fixture div:has(faketag, div:has(faketag, div:not([id])))",
[ "moretests", "t2037", "fx-test-group", "fx-queue" ] );
} );

QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "pseudo - contains", function( assert ) {
Expand Down
15 changes: 10 additions & 5 deletions test/unit/support.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,24 @@ testIframe(
userAgent = window.navigator.userAgent,
expectedMap = {
ie_11: {
"reliableTrDimensions": false
cssSupportsSelector: false,
reliableTrDimensions: false
},
chrome: {
"reliableTrDimensions": true
cssSupportsSelector: false,
reliableTrDimensions: true
},
safari: {
"reliableTrDimensions": true
cssSupportsSelector: false,
reliableTrDimensions: true
},
firefox: {
"reliableTrDimensions": false
cssSupportsSelector: false,
reliableTrDimensions: false
},
ios: {
"reliableTrDimensions": true
cssSupportsSelector: false,
reliableTrDimensions: true
}
};

Expand Down