Skip to content

Commit

Permalink
feat(utils/matches): support selectors level 4 :not and :is (#2742)
Browse files Browse the repository at this point in the history
* feat(utils/matches): support selector-v4 :not and :is

* fix typo

* Update test/core/utils/matches.js

Co-authored-by: Wilco Fiers <[email protected]>

* test

Co-authored-by: Wilco Fiers <[email protected]>
  • Loading branch information
straker and WilcoFiers authored Jan 11, 2021
1 parent 1d864b4 commit 21d9b0e
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 30 deletions.
2 changes: 1 addition & 1 deletion lib/checks/keyboard/page-has-heading-one.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"evaluate": "has-descendant-evaluate",
"after": "has-descendant-after",
"options": {
"selector": "h1:not([role]):not([aria-level]), h1:not([role])[aria-level=1], h2:not([role])[aria-level=1], h3:not([role])[aria-level=1], h4:not([role])[aria-level=1], h5:not([role])[aria-level=1], h6:not([role])[aria-level=1], [role=heading][aria-level=1]"
"selector": "h1:not([role], [aria-level]), :is(h1, h2, h3, h4, h5, h6):not([role])[aria-level=1], [role=heading][aria-level=1]"
},
"metadata": {
"impact": "moderate",
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/navigation/header-present.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"evaluate": "has-descendant-evaluate",
"after": "has-descendant-after",
"options": {
"selector": "h1:not([role]), h2:not([role]), h3:not([role]), h4:not([role]), h5:not([role]), h6:not([role]), [role=heading]"
"selector": ":is(h1, h2, h3, h4, h5, h6):not([role]), [role=heading]"
},
"metadata": {
"impact": "serious",
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/css-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CssSelectorParser } from 'css-selector-parser';

const parser = new CssSelectorParser();
parser.registerSelectorPseudos('not');
parser.registerSelectorPseudos('is');
parser.registerNestingOperators('>');
parser.registerAttrEqualityMods('^', '$', '*', '~');

Expand Down
10 changes: 8 additions & 2 deletions lib/core/utils/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ function matchesPseudos(target, exp) {
!exp.pseudos ||
exp.pseudos.every(pseudo => {
if (pseudo.name === 'not') {
return !matchesExpression(target, pseudo.expressions[0]);
return !pseudo.expressions.some(expression => {
return matchesExpression(target, expression);
});
} else if (pseudo.name === 'is') {
return pseudo.expressions.some(expression => {
return matchesExpression(target, expression);
});
}
throw new Error(
'the pseudo selector ' + pseudo.name + ' has not yet been implemented'
Expand Down Expand Up @@ -148,7 +154,7 @@ function convertPseudos(pseudos) {
return pseudos.map(p => {
var expressions;

if (p.name === 'not') {
if (['is', 'not'].includes(p.name)) {
expressions = p.value;
expressions = expressions.selectors
? expressions.selectors
Expand Down
9 changes: 7 additions & 2 deletions lib/rules/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,13 @@ function colorContrastMatches(node, virtualNode) {

// implicit label of disabled control
const query =
'input:not([type="hidden"]):not([type="image"])' +
':not([type="button"]):not([type="submit"]):not([type="reset"]), select, textarea';
'input:not(' +
'[type="hidden"],' +
'[type="image"],' +
'[type="button"],' +
'[type="submit"],' +
'[type="reset"]' +
'), select, textarea';
const implicitControl = querySelectorAll(labelVirtual, query)[0];

if (implicitControl && isDisabled(implicitControl)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/role-img-alt.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "role-img-alt",
"selector": "[role='img']:not(img):not(area):not(input):not(object)",
"selector": "[role='img']:not(img, area, input, object)",
"matches": "html-namespace-matches",
"tags": [
"cat.text-alternatives",
Expand Down
102 changes: 79 additions & 23 deletions test/core/utils/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,38 +132,94 @@ describe('utils.matches', function() {
});

describe('pseudos', function() {
it('returns true if :not matches using tag', function() {
it('throws error if pseudo is not implemented', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(span)'));
assert.throws(function() {
matches(virtualNode, 'h1:empty');
});
assert.throws(function() {
matches(virtualNode, 'h1::before');
});
});

it('returns true if :not matches using class', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(.foo)'));
});
describe(':not', function() {
it('returns true if :not matches using tag', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(span)'));
});

it('returns true if :not matches using attribute', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not([class])'));
});
it('returns true if :not matches using class', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(.foo)'));
});

it('returns true if :not matches using id', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(#foo)'));
});
it('returns true if :not matches using attribute', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not([class])'));
});

it('returns false if :not matches element', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isFalse(matches(virtualNode, 'h1:not([id])'));
it('returns true if :not matches using id', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not(#foo)'));
});

it('returns true if :not matches none of the selectors', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:not([role=heading], span)'));
});

it('returns false if :not matches element', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isFalse(matches(virtualNode, 'h1:not([id])'));
});

it('returns false if :not matches one of the selectors', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isFalse(matches(virtualNode, 'h1:not([role=heading], [id])'));
});
});

it('throws error if pseudo is not implemented', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.throws(function() {
matches(virtualNode, 'h1:empty');
describe(':is', function() {
it('returns true if :is matches using tag', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, ':is(h1)'));
});
assert.throws(function() {
matches(virtualNode, 'h1::before');

it('returns true if :is matches using class', function() {
var virtualNode = queryFixture('<h1 id="target" class="foo">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:is(.foo)'));
});

it('returns true if :is matches using attribute', function() {
var virtualNode = queryFixture('<h1 id="target" class="foo">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:is([class])'));
});

it('returns true if :is matches using id', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, 'h1:is(#target)'));
});

it('returns true if :is matches one of the selectors', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isTrue(matches(virtualNode, ':is([role=heading], h1)'));
});

it('returns true if :is matches complex selector', function() {
var virtualNode = queryFixture('<div><h1 id="target">foo</h1></div>');
assert.isTrue(matches(virtualNode, 'h1:is(div > #target)'));
});

it('returns false if :is does not match element', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isFalse(matches(virtualNode, 'h1:is([class])'));
});

it('returns false if :is matches none of the selectors', function() {
var virtualNode = queryFixture('<h1 id="target">foo</h1>');
assert.isFalse(
matches(virtualNode, 'h1:is([class], span, #foo, .bar)')
);
});
});
});
Expand Down

0 comments on commit 21d9b0e

Please sign in to comment.