-
Notifications
You must be signed in to change notification settings - Fork 779
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
Sd/region check #450
Sd/region check #450
Changes from 4 commits
0252218
02daad1
ecd222f
127c0bc
08509b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,51 @@ | ||
//jshint latedef: false | ||
const { dom, aria } = axe.commons; | ||
function getSkiplink (virtualNode) { | ||
const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0]; | ||
if (firstLink && axe.commons.dom.getElementByReference(firstLink.actualNode, 'href')) { | ||
return firstLink.actualNode; | ||
} | ||
} | ||
|
||
var landmarkRoles = axe.commons.aria.getRolesByType('landmark'), | ||
firstLink = node.querySelector('a[href]'); | ||
const skipLink = getSkiplink(virtualNode); | ||
const landmarkRoles = aria.getRolesByType('landmark'); | ||
const implicitLandmarks = landmarkRoles | ||
.reduce((arr, role) => arr.concat(aria.implicitNodes(role)), []) | ||
.filter(r => r !== null).map(r => r.toUpperCase()); | ||
|
||
function isSkipLink(n) { | ||
return firstLink && | ||
axe.commons.dom.isFocusable(axe.commons.dom.getElementByReference(firstLink, 'href')) && | ||
firstLink === n; | ||
// Check if the current element it the skiplink | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo in the comment: |
||
function isSkipLink (node) { | ||
return skipLink && skipLink === node; | ||
} | ||
|
||
function isLandmark(n) { | ||
var role = n.getAttribute('role'); | ||
return role && (landmarkRoles.indexOf(role) !== -1); | ||
// Check if the current element is a landmark | ||
function isLandmark (node) { | ||
const nodeName = node.nodeName.toUpperCase(); | ||
return (landmarkRoles.includes(node.getAttribute('role')) || | ||
implicitLandmarks.includes(nodeName)); | ||
} | ||
|
||
function checkRegion(n) { | ||
if (isLandmark(n)) { return null; } | ||
if (isSkipLink(n)) { return getViolatingChildren(n); } | ||
if (axe.commons.dom.isVisible(n, true) && | ||
(axe.commons.text.visible(n, true, true) || axe.commons.dom.isVisualContent(n))) { return n; } | ||
return getViolatingChildren(n); | ||
} | ||
function getViolatingChildren(n) { | ||
var children = axe.commons.utils.toArray(n.children); | ||
if (children.length === 0) { return []; } | ||
return children.map(checkRegion) | ||
.filter(function (c) { return c !== null; }) | ||
.reduce(function (a, b) { return a.concat(b); }, []); | ||
/** | ||
* Find all visible elements not wrapped inside a landmark or skiplink | ||
*/ | ||
function findRegionlessElms (virtualNode) { | ||
const node = virtualNode.actualNode; | ||
// End recursion if the element a landmark, skiplink, or hidden content | ||
if (isLandmark(node) || isSkipLink(node) || !dom.isVisible(node, true)) { | ||
return []; | ||
|
||
// Return the node is a content element | ||
} else if (dom.hasContent(node, /* noRecursion: */ true)) { | ||
return [node]; | ||
|
||
// Recursively look at all child elements | ||
} else { | ||
return virtualNode.children.filter(({ actualNode }) => actualNode.nodeType === 1) | ||
.map(findRegionlessElms) | ||
.reduce((a, b) => a.concat(b), []); // flatten the results | ||
} | ||
} | ||
|
||
var v = getViolatingChildren(node); | ||
this.relatedNodes(v); | ||
return !v.length; | ||
var regionlessNodes = findRegionlessElms(virtualNode); | ||
this.relatedNodes(regionlessNodes); | ||
|
||
return regionlessNodes.length === 0; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,20 @@ | ||
/*global dom */ | ||
|
||
dom.getElementByReference = function (node, attr) { | ||
'use strict'; | ||
|
||
var candidate, | ||
fragment = node.getAttribute(attr), | ||
doc = document; | ||
let fragment = node.getAttribute(attr); | ||
|
||
if (fragment && fragment.charAt(0) === '#') { | ||
fragment = fragment.substring(1); | ||
|
||
candidate = doc.getElementById(fragment); | ||
let candidate = document.getElementById(fragment); | ||
if (candidate) { | ||
return candidate; | ||
} | ||
|
||
candidate = doc.getElementsByName(fragment); | ||
candidate = document.getElementsByName(fragment); | ||
if (candidate.length) { | ||
return candidate[0]; | ||
} | ||
|
||
} | ||
|
||
return null; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,70 +2,62 @@ describe('region', function () { | |
'use strict'; | ||
|
||
var fixture = document.getElementById('fixture'); | ||
var shadowSupport = axe.testUtils.shadowSupport; | ||
var checkSetup = axe.testUtils.checkSetup; | ||
|
||
var checkContext = { | ||
_relatedNodes: [], | ||
_data: null, | ||
data: function (d) { | ||
this._data = d; | ||
}, | ||
relatedNodes: function (rn) { | ||
this._relatedNodes = rn; | ||
} | ||
}; | ||
var checkContext = new axe.testUtils.MockCheckContext(); | ||
|
||
afterEach(function () { | ||
fixture.innerHTML = ''; | ||
checkContext._relatedNodes = []; | ||
checkContext._data = null; | ||
checkContext.reset(); | ||
}); | ||
|
||
it('should return true when all content is inside the region', function () { | ||
fixture.innerHTML = '<div id="target"><div role="main"><a href="a.html#mainheader">Click Here</a><div><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isTrue(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><div role="main"><a href="a.html#mainheader">Click Here</a><div><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 0); | ||
}); | ||
|
||
it('should return false when img content is outside the region', function () { | ||
fixture.innerHTML = '<div id="target"><img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isFalse(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'); | ||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 1); | ||
}); | ||
|
||
it('should return true when textless text content is outside the region', function () { | ||
fixture.innerHTML = '<div id="target"><p></p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isTrue(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><p></p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 0); | ||
}); | ||
|
||
it('should return true when wrapper content is outside the region', function () { | ||
fixture.innerHTML = '<div id="target"><div><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isTrue(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><div><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 0); | ||
}); | ||
|
||
it('should return true when invisible content is outside the region', function () { | ||
fixture.innerHTML = '<div id="target"><p style="display: none">Click Here</p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isTrue(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><p style="display: none">Click Here</p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 0); | ||
}); | ||
|
||
it('should return true when there is a skiplink', function () { | ||
fixture.innerHTML = '<div id="target"><a href="#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isTrue(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><a href="#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 0); | ||
}); | ||
|
||
it('should return false when there is a non-region element', function () { | ||
fixture.innerHTML = '<div id="target"><div>This is random content.</div><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isFalse(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><div>This is random content.</div><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'); | ||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 1); | ||
}); | ||
|
||
|
@@ -74,9 +66,75 @@ describe('region', function () { | |
}); | ||
|
||
it('should return false when there is a non-skiplink', function () { | ||
fixture.innerHTML = '<div id="target"><a href="something.html#mainheader">Click Here</a><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'; | ||
var node = fixture.querySelector('#target'); | ||
assert.isFalse(checks.region.evaluate.call(checkContext, node)); | ||
var checkArgs = checkSetup('<div id="target"><a href="something.html#mainheader">Click Here</a><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'); | ||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.equal(checkContext._relatedNodes.length, 1); | ||
}); | ||
|
||
it('should return true if the non-region element is a script', function () { | ||
var checkArgs = checkSetup('<div id="target"><script>axe.run()</script><div role="main">Content</div></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
}); | ||
|
||
it('should considered aria labelled elements as content', function () { | ||
var checkArgs = checkSetup('<div id="target"><div aria-label="axe-core logo"></div><div role="main">Content</div></div>'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this DIV have a role on it for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't matter for the test, but I think you're right. Adding role="img". |
||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.deepEqual(checkContext._relatedNodes, [ | ||
fixture.querySelector('div[aria-label]') | ||
]); | ||
}); | ||
|
||
it('should allow native landmark elements', function () { | ||
var checkArgs = checkSetup('<div id="target"><header>branding</header><main>Content </main><aside>stuff</aside><footer>copyright</footer></div>'); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.lengthOf(checkContext._relatedNodes, 0); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)('should test Shadow tree content', function () { | ||
var div = document.createElement('div'); | ||
var shadow = div.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = 'Some text'; | ||
var checkArgs = checkSetup(div); | ||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.deepEqual(checkContext._relatedNodes, [div]); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)('should test slotted content', function () { | ||
var div = document.createElement('div'); | ||
div.innerHTML = 'Some content'; | ||
var shadow = div.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = '<div role="main"><slot></slot></div>'; | ||
var checkArgs = checkSetup(div); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.lengthOf(checkContext._relatedNodes, 0); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)('should ignore skiplink targets inside shadow trees', function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, good one. |
||
var div = document.createElement('div'); | ||
div.innerHTML = '<a href="#foo">skiplink</a><div>Content</div>'; | ||
|
||
var shadow = div.querySelector('div').attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = '<div role="main" id=#foo"><slot></slot></div>'; | ||
var checkArgs = checkSetup(div); | ||
|
||
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.deepEqual(checkContext._relatedNodes, [div.querySelector('a')]); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)('should find the skiplink in shadow DOM', function () { | ||
var div = document.createElement('div'); | ||
div.innerHTML = '<span id="foo">Content!</span>'; | ||
var shadow = div.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = '<a href="#foo">skiplink</a><div role="main"><slot></slot></div>'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The href is inside the shadow DOM, but the IDref is in the light DOM...? How does that work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
var checkArgs = checkSetup(div); | ||
|
||
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs)); | ||
assert.lengthOf(checkContext._relatedNodes, 0); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,13 @@ describe('dom.hasContent', function () { | |
); | ||
}); | ||
|
||
it('is false if noRecurstion is true and the content is not in a child', function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: |
||
fixture.innerHTML = '<div id="target"><span> text </span></div>'; | ||
tree = axe.utils.getFlattenedTree(fixture); | ||
|
||
assert.isFalse(hasContent(axe.utils.querySelectorAll(tree, '#target')[0], true)); | ||
}); | ||
|
||
(shadowSupport ? it : xit)('looks at content of shadow dom elements', function () { | ||
fixture.innerHTML = '<div id="target"></div>'; | ||
var shadow = fixture.querySelector('#target').attachShadow({ mode: 'open' }); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,23 @@ | ||
var testUtils = {}; | ||
|
||
testUtils.MockCheckContext = function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
'use strict'; | ||
return { | ||
_relatedNodes: [], | ||
_data: null, | ||
data: function (d) { | ||
this._data = d; | ||
}, | ||
relatedNodes: function (rn) { | ||
this._relatedNodes = rn; | ||
}, | ||
reset: function () { | ||
this._data = null; | ||
this._relatedNodes = []; | ||
} | ||
}; | ||
}; | ||
|
||
testUtils.shadowSupport = (function(document) { | ||
'use strict'; | ||
var v0 = document.body && typeof document.body.createShadowRoot === 'function', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment about how this code works?