-
Notifications
You must be signed in to change notification settings - Fork 791
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rule): Scrollable region focusable (#1396)
- Loading branch information
Showing
20 changed files
with
639 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* Note: | ||
* Check if given node contains focusable elements (excluding thyself) | ||
*/ | ||
const tabbableElements = virtualNode.tabbableElements; | ||
|
||
if (!tabbableElements) { | ||
return false; | ||
} | ||
|
||
// remove thyself from tabbable elements (if exists) | ||
const tabbableContentElements = tabbableElements.filter( | ||
el => el !== virtualNode | ||
); | ||
|
||
return tabbableContentElements.length > 0; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"id": "focusable-content", | ||
"evaluate": "focusable-content.js", | ||
"metadata": { | ||
"impact": "moderate", | ||
"messages": { | ||
"pass": "Element contains focusable elements", | ||
"fail": "Element should have focusable content" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* Note: | ||
* Check | ||
* - if element is focusable | ||
* - if element is in focus order via `tabindex` | ||
*/ | ||
const isFocusable = virtualNode.isFocusable; | ||
|
||
let tabIndex = parseInt(virtualNode.actualNode.getAttribute('tabindex'), 10); | ||
tabIndex = !isNaN(tabIndex) ? tabIndex : null; | ||
|
||
return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"id": "focusable-element", | ||
"evaluate": "focusable-element.js", | ||
"metadata": { | ||
"impact": "moderate", | ||
"messages": { | ||
"pass": "Element is focusable", | ||
"fail": "Element should be focusable" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* Get the scroll position of given element | ||
* @method getScroll | ||
* @memberof axe.utils | ||
* @param {Element} node | ||
* @param {buffer} (Optional) allowed negligence in overflow | ||
* @returns {Object | undefined} | ||
*/ | ||
axe.utils.getScroll = function getScroll(elm, buffer = 0) { | ||
const overflowX = elm.scrollWidth > elm.clientWidth + buffer; | ||
const overflowY = elm.scrollHeight > elm.clientHeight + buffer; | ||
|
||
/** | ||
* if there is neither `overflow-x` or `overflow-y` | ||
* -> return | ||
*/ | ||
if (!(overflowX || overflowY)) { | ||
return; | ||
} | ||
|
||
const style = window.getComputedStyle(elm); | ||
const scrollableX = style.getPropertyValue('overflow-x') !== 'visible'; | ||
const scrollableY = style.getPropertyValue('overflow-y') !== 'visible'; | ||
|
||
/** | ||
* check direction of `overflow` and `scrollable` | ||
*/ | ||
if ((overflowX && scrollableX) || (overflowY && scrollableY)) { | ||
return { | ||
elm, | ||
top: elm.scrollTop, | ||
left: elm.scrollLeft | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/** | ||
* Note: | ||
* `excludeHidden=true` for this rule, thus considering only elements in the accessibility tree. | ||
*/ | ||
const { querySelectorAll } = axe.utils; | ||
const { hasContentVirtual } = axe.commons.dom; | ||
|
||
/** | ||
* if not scrollable -> `return` | ||
*/ | ||
if (!!axe.utils.getScroll(node, 13) === false) { | ||
return false; | ||
} | ||
|
||
/** | ||
* check if node has visible contents | ||
*/ | ||
const nodeAndDescendents = querySelectorAll(virtualNode, '*'); | ||
const hasVisibleChildren = nodeAndDescendents.some(elm => | ||
hasContentVirtual( | ||
elm, | ||
true, // noRecursion | ||
true // ignoreAria | ||
) | ||
); | ||
if (!hasVisibleChildren) { | ||
return false; | ||
} | ||
|
||
return true; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"id": "scrollable-region-focusable", | ||
"matches": "scrollable-region-focusable-matches.js", | ||
"tags": ["wcag2a", "wcag211"], | ||
"metadata": { | ||
"description": "Elements that have scrollable content should be accessible by keyboard", | ||
"help": "Ensure that scrollable region has keyboard access" | ||
}, | ||
"all": [], | ||
"any": ["focusable-content", "focusable-element"], | ||
"none": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
describe('focusable-content tests', function() { | ||
'use strict'; | ||
|
||
var check; | ||
var fixture = document.getElementById('fixture'); | ||
var fixtureSetup = axe.testUtils.fixtureSetup; | ||
var shadowSupported = axe.testUtils.shadowSupport.v1; | ||
var checkContext = axe.testUtils.MockCheckContext(); | ||
var checkSetup = axe.testUtils.checkSetup; | ||
|
||
before(function() { | ||
check = checks['focusable-content']; | ||
}); | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
axe._tree = undefined; | ||
checkContext.reset(); | ||
}); | ||
|
||
it('returns false when there are no focusable content elements (content element `div` is not focusable)', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + '<div> Content </div>' + '</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns false when content element is taken out of focusable order (tabindex = -1)', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + '<input type="text" tabindex="-1">' + '</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns false when element is focusable (only checks if contents are focusable)', function() { | ||
var params = checkSetup( | ||
'<div id="target" tabindex="0">' + | ||
'<p style="height: 200px;"></p>' + | ||
'</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns false when all content elements are not focusable', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + | ||
'<input type="text" tabindex="-1">' + | ||
'<select tabindex="-1"></select>' + | ||
'<textarea tabindex="-1"></textarea>' + | ||
'</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns true when one deeply nested content element is focusable', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + | ||
'<div style="height: 200px"> ' + | ||
'<div style="height: 200px">' + | ||
'<input type="text">' + | ||
'</div>' + | ||
'</div>' + | ||
'</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isTrue(actual); | ||
}); | ||
|
||
it('returns true when content element can be focused', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + '<input type="text">' + '</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isTrue(actual); | ||
}); | ||
|
||
it('returns true when any one of the many content elements can be focused', function() { | ||
var params = checkSetup( | ||
'<div id="target">' + | ||
'<input type="text" tabindex="-1">' + | ||
'<select tabindex="-1"></select>' + | ||
'<textarea tabindex="-1"></textarea>' + | ||
'<p style="height: 200px;" tabindex="0"></p>' + | ||
'</div>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isTrue(actual); | ||
}); | ||
|
||
describe('shadowDOM - focusable content', function() { | ||
before(function() { | ||
if (!shadowSupported) { | ||
this.skip(); | ||
} | ||
}); | ||
|
||
it('returns true when content element can be focused', function() { | ||
fixtureSetup('<div id="target">' + '</div>'); | ||
var node = fixture.querySelector('#target'); | ||
var shadow = node.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = '<input type="text">'; | ||
axe._tree = axe.utils.getFlattenedTree(fixture); | ||
axe._selectorData = axe.utils.getSelectorData(axe._tree); | ||
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); | ||
var actual = check.evaluate.call(checkContext, node, {}, virtualNode); | ||
assert.isTrue(actual); | ||
}); | ||
|
||
it('returns false when no focusable content', function() { | ||
fixtureSetup('<div id="target">' + '</div>'); | ||
var node = fixture.querySelector('#target'); | ||
var shadow = node.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = | ||
'<input type="text" tabindex="-1"> <p>just some text</p>'; | ||
axe._tree = axe.utils.getFlattenedTree(fixture); | ||
axe._selectorData = axe.utils.getSelectorData(axe._tree); | ||
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); | ||
var actual = check.evaluate.call(checkContext, node, {}, virtualNode); | ||
assert.isFalse(actual); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
describe('focusable-element tests', function() { | ||
'use strict'; | ||
|
||
var check; | ||
var fixture = document.getElementById('fixture'); | ||
var checkContext = axe.testUtils.MockCheckContext(); | ||
var checkSetup = axe.testUtils.checkSetup; | ||
|
||
before(function() { | ||
check = checks['focusable-element']; | ||
}); | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
axe._tree = undefined; | ||
checkContext.reset(); | ||
}); | ||
|
||
it('returns true when element is focusable', function() { | ||
var params = checkSetup('<input id="target" type="radio">'); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isTrue(actual); | ||
}); | ||
|
||
it('returns false when element made not focusable by tabindex', function() { | ||
var params = checkSetup( | ||
'<input id="target" type="checkbox" tabindex="-1">' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns false when element is not focusable by default', function() { | ||
var params = checkSetup('<p id="target">I hold some text </p>'); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isFalse(actual); | ||
}); | ||
|
||
it('returns true when element made focusable by tabindex', function() { | ||
var params = checkSetup( | ||
'<p id="target" tabindex="0">I hold some text </p>' | ||
); | ||
var actual = check.evaluate.apply(checkContext, params); | ||
assert.isTrue(actual); | ||
}); | ||
}); |
Oops, something went wrong.