Skip to content

Commit

Permalink
fix(scrollable-region-focusalbe): do not fail for combobox pattern (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
straker authored Oct 28, 2020
1 parent 926b6a8 commit ac71a57
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 2 deletions.
2 changes: 1 addition & 1 deletion lib/core/utils/css-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { CssSelectorParser } from 'css-selector-parser';
const parser = new CssSelectorParser();
parser.registerSelectorPseudos('not');
parser.registerNestingOperators('>');
parser.registerAttrEqualityMods('^', '$', '*');
parser.registerAttrEqualityMods('^', '$', '*', '~');

export default parser;
42 changes: 41 additions & 1 deletion lib/rules/scrollable-region-focusable-matches.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { hasContentVirtual } from '../commons/dom';
import { querySelectorAll, getScroll } from '../core/utils';
import { getExplicitRole } from '../commons/aria';
import standards from '../standards';
import {
querySelectorAll,
getScroll,
closest,
getRootNode,
tokenList
} from '../core/utils';

function scrollableRegionFocusableMatches(node, virtualNode) {
/**
Expand All @@ -14,6 +22,38 @@ function scrollableRegionFocusableMatches(node, virtualNode) {
return false;
}

/**
* ignore scrollable regions owned by combobox. limit to roles
* ownable by combobox so we don't keep calling closest for every
* node (which would be slow)
* @see https://github.com/dequelabs/axe-core/issues/1763
*/
const role = getExplicitRole(virtualNode);
if (standards.ariaRoles.combobox.requiredOwned.includes(role)) {
// in ARIA 1.1 the container has role=combobox
if (closest(virtualNode, '[role~="combobox"]')) {
return false;
}

// in ARIA 1.0 and 1.2 the combobox owns (1.0) or controls (1.2)
// the listbox
const id = virtualNode.attr('id');
if (id) {
const doc = getRootNode(node);
const owned = Array.from(
doc.querySelectorAll(`[aria-owns~="${id}"], [aria-controls~="${id}"]`)
);
const comboboxOwned = owned.some(el => {
const roles = tokenList(el.getAttribute('role'));
return roles.includes('combobox');
});

if (comboboxOwned) {
return false;
}
}
}

/**
* check if node has visible contents
*/
Expand Down
28 changes: 28 additions & 0 deletions test/core/utils/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,34 @@ describe('utils.matches', function() {
);
assert.isFalse(matches(virtualNode, '[foo="baz"][bar="foo"][baz="bar"]'));
});

it('returns true if attribute starts with value', function() {
var virtualNode = queryFixture(
'<span id="target" foo="bazaphone" bar="foo" baz="bar"></span>'
);
assert.isTrue(matches(virtualNode, '[foo^="baz"]'));
});

it('returns true if attribute ends with value', function() {
var virtualNode = queryFixture(
'<span id="target" foo="bazaphone" bar="foo" baz="bar"></span>'
);
assert.isTrue(matches(virtualNode, '[foo$="hone"]'));
});

it('returns true if attribute contains value', function() {
var virtualNode = queryFixture(
'<span id="target" foo="bazaphone" bar="foo" baz="bar"></span>'
);
assert.isTrue(matches(virtualNode, '[foo*="baz"]'));
});

it('returns true if attribute has value', function() {
var virtualNode = queryFixture(
'<span id="target" foo="bar baz" bar="foo" baz="bar"></span>'
);
assert.isTrue(matches(virtualNode, '[foo~="baz"]'));
});
});

describe('id', function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,35 @@
<p>Content</p>
</div>
</div>

<div role="combobox">
<div
id="inapplicable7"
role="listbox"
style="height: 200px; overflow-y: auto"
>
<div role="option" style="height: 2000px">
<p>Content</p>
</div>
</div>
</div>

<div role="combobox" aria-owns="inapplicable8">
<div id="inapplicable8" role="grid" style="height: 200px; overflow-y: auto">
<div role="option" style="height: 2000px">
<p>Content</p>
</div>
</div>

<div role="combobox" aria-controls="inapplicable9">
<div
id="inapplicable9"
role="dialog"
style="height: 200px; overflow-y: auto"
>
<div role="option" style="height: 2000px">
<p>Content</p>
</div>
</div>
</div>
</div>
56 changes: 56 additions & 0 deletions test/rule-matches/scrollable-region-focusable-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,62 @@ describe('scrollable-region-focusable-matches', function() {
assert.isFalse(actual);
});

it('returns false when element has combobox ancestor', function() {
var target = queryFixture(
'<div role="combobox"><ul id="target" role="listbox" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul></div>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns false when element is owned by combobox', function() {
var target = queryFixture(
'<input role="combobox" aria-owns="foo target"/><ul id="target" role="listbox" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns false when element is controlled by combobox', function() {
var target = queryFixture(
'<input role="combobox" aria-controls="foo target"/><ul id="target" role="listbox" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns false for combobox with tree', function() {
var target = queryFixture(
'<div role="combobox"><ul id="target" role="tree" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul></div>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns false for combobox with grid', function() {
var target = queryFixture(
'<div role="combobox"><ul id="target" role="grid" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul></div>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns false for combobox with dialog', function() {
var target = queryFixture(
'<div role="combobox"><ul id="target" role="dialog" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul></div>'
);
var actual = rule.matches(target.actualNode, target);
assert.isFalse(actual);
});

it('returns true for combobox with non-valid role', function() {
var target = queryFixture(
'<div role="combobox"><ul id="target" role="section" style="width: 12em; height: 2em; border: dotted; overflow: scroll;"><li role="option" style="height: 15rem">Option</li></ul></div>'
);
var actual = rule.matches(target.actualNode, target);
assert.isTrue(actual);
});

describe('shadowDOM - scrollable-region-focusable-matches', function() {
before(function() {
if (!shadowSupported) {
Expand Down

0 comments on commit ac71a57

Please sign in to comment.