diff --git a/lib/checks/navigation/region.js b/lib/checks/navigation/region.js
index 653146f21f..e890bea5ab 100644
--- a/lib/checks/navigation/region.js
+++ b/lib/checks/navigation/region.js
@@ -1,17 +1,4 @@
const { dom, aria } = axe.commons;
-
-// Return the skplink, if any
-function getSkiplink(virtualNode) {
- const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0];
- if (
- firstLink &&
- axe.commons.dom.getElementByReference(firstLink.actualNode, 'href')
- ) {
- return firstLink.actualNode;
- }
-}
-
-const skipLink = getSkiplink(virtualNode);
const landmarkRoles = aria.getRolesByType('landmark');
// Create a list of nodeNames that have a landmark as an implicit role
@@ -19,11 +6,6 @@ const implicitLandmarks = landmarkRoles
.reduce((arr, role) => arr.concat(aria.implicitNodes(role)), [])
.filter(r => r !== null);
-// Check if the current element is the skiplink
-function isSkipLink(vNode) {
- return skipLink && skipLink === vNode.actualNode;
-}
-
// Check if the current element is a landmark
function isRegion(virtualNode) {
const node = virtualNode.actualNode;
@@ -61,7 +43,8 @@ function findRegionlessElms(virtualNode) {
// End recursion if the element is a landmark, skiplink, or hidden content
if (
isRegion(virtualNode) ||
- isSkipLink(virtualNode) ||
+ (dom.isSkipLink(virtualNode.actualNode) &&
+ dom.getElementByReference(virtualNode.actualNode, 'href')) ||
!dom.isVisible(node, true)
) {
return [];
diff --git a/lib/commons/dom/is-skip-link.js b/lib/commons/dom/is-skip-link.js
new file mode 100644
index 0000000000..e9fd5a100a
--- /dev/null
+++ b/lib/commons/dom/is-skip-link.js
@@ -0,0 +1,37 @@
+/* global dom */
+
+// test for hrefs that start with # or /# (for angular)
+const isInternalLinkRegex = /^\/?#[^/!]/;
+
+/**
+ * Determines if element is a skip link
+ * @method isSkipLink
+ * @memberof axe.commons.dom
+ * @instance
+ * @param {Element} element
+ * @return {Boolean}
+ */
+dom.isSkipLink = function(element) {
+ if (!isInternalLinkRegex.test(element.getAttribute('href'))) {
+ return false;
+ }
+
+ // define a skip link as any anchor element whose href starts with `#...`
+ // and which precedes the first anchor element whose href doesn't start
+ // with `#...` (that is, a link to a page)
+ const firstPageLink = axe.utils.querySelectorAll(
+ axe._tree,
+ 'a:not([href^="#"]):not([href^="/#"]):not([href^="javascript"])'
+ )[0];
+
+ // if there are no page links then all all links will need to be
+ // considered as skip links
+ if (!firstPageLink) {
+ return true;
+ }
+
+ return (
+ element.compareDocumentPosition(firstPageLink.actualNode) ===
+ element.DOCUMENT_POSITION_FOLLOWING
+ );
+};
diff --git a/lib/rules/skip-link-matches.js b/lib/rules/skip-link-matches.js
index 24bab16507..c642f0d443 100644
--- a/lib/rules/skip-link-matches.js
+++ b/lib/rules/skip-link-matches.js
@@ -1 +1 @@
-return /^#[^/!]/.test(node.getAttribute('href'));
+return axe.commons.dom.isSkipLink(node);
diff --git a/lib/rules/skip-link.json b/lib/rules/skip-link.json
index c47905ea6f..7d7e5d71b4 100644
--- a/lib/rules/skip-link.json
+++ b/lib/rules/skip-link.json
@@ -1,6 +1,6 @@
{
"id": "skip-link",
- "selector": "a[href]",
+ "selector": "a[href^=\"#\"], a[href^=\"/#\"]",
"matches": "skip-link-matches.js",
"tags": ["cat.keyboard", "best-practice"],
"metadata": {
diff --git a/test/checks/navigation/skip-link.js b/test/checks/navigation/skip-link.js
index d44f3ae71b..1b9eac0a27 100644
--- a/test/checks/navigation/skip-link.js
+++ b/test/checks/navigation/skip-link.js
@@ -7,33 +7,28 @@ describe('skip-link', function() {
fixture.innerHTML = '';
});
- it('should return false if the href points to another document', function() {
- fixture.innerHTML =
- 'Click Here
Introduction
';
- var node = fixture.querySelector('a');
- assert.isFalse(checks['skip-link'].evaluate(node));
- });
-
- it('should return false if the href points to a non-existent element', function() {
- fixture.innerHTML =
- 'Click Here
Introduction
';
- var node = fixture.querySelector('a');
- assert.isFalse(checks['skip-link'].evaluate(node));
- });
-
it('should return true if the href points to an element with an ID', function() {
fixture.innerHTML =
'Click Here
Introduction
';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
var node = fixture.querySelector('a');
assert.isTrue(checks['skip-link'].evaluate(node));
});
it('should return true if the href points to an element with an name', function() {
fixture.innerHTML = 'Click Here';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
var node = fixture.querySelector('a');
assert.isTrue(checks['skip-link'].evaluate(node));
});
+ it('should return false if the href points to a non-existent element', function() {
+ fixture.innerHTML =
+ 'Click Here
Introduction
';
+ var node = fixture.querySelector('a');
+ assert.isFalse(checks['skip-link'].evaluate(node));
+ });
+
it('should return undefined if the target has display:none', function() {
fixture.innerHTML =
'Click Here' +
@@ -49,18 +44,4 @@ describe('skip-link', function() {
var node = fixture.querySelector('a');
assert.isUndefined(checks['skip-link'].evaluate(node));
});
-
- it('should return true if the URI encoded href points to an element with an ID', function() {
- fixture.innerHTML =
- 'Click Here
Introduction
';
- var node = fixture.querySelector('a');
- assert.isTrue(checks['skip-link'].evaluate(node));
- });
-
- it('should return true if the URI is an Angular skiplink', function() {
- fixture.innerHTML =
- 'Click Here
Introduction
';
- var node = fixture.querySelector('a');
- assert.isTrue(checks['skip-link'].evaluate(node));
- });
});
diff --git a/test/commons/dom/is-skip-link.js b/test/commons/dom/is-skip-link.js
new file mode 100644
index 0000000000..e28bad9adb
--- /dev/null
+++ b/test/commons/dom/is-skip-link.js
@@ -0,0 +1,71 @@
+describe('dom.isSkipLink', function() {
+ 'use strict';
+
+ var fixture = document.getElementById('fixture');
+
+ afterEach(function() {
+ fixture.innerHTML = '';
+ });
+
+ it('should return true if the href points to an ID', function() {
+ fixture.innerHTML = 'Click Here';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('a');
+ assert.isTrue(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should return false if the href points to another document', function() {
+ fixture.innerHTML = 'Click Here';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('a');
+ assert.isFalse(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should return true if the URI encoded href points to an element with an ID', function() {
+ fixture.innerHTML = 'Click Here';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('a');
+ assert.isTrue(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should return true if the URI is an Angular skiplink', function() {
+ fixture.innerHTML = 'Click Here';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('a');
+ assert.isTrue(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should return true for multiple skip-links', function() {
+ fixture.innerHTML =
+ 'Click Here>Click Here>Click Here>';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var nodes = fixture.querySelectorAll('a');
+ for (var i = 0; i < nodes.length; i++) {
+ assert.isTrue(axe.commons.dom.isSkipLink(nodes[i]));
+ }
+ });
+
+ it('should return true if the element is before a page link', function() {
+ fixture.innerHTML =
+ 'Click Here>New Page';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('#skip-link');
+ assert.isTrue(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should return false if the element is after a page link', function() {
+ fixture.innerHTML =
+ 'New PageClick Here>';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('#skip-link');
+ assert.isFalse(axe.commons.dom.isSkipLink(node));
+ });
+
+ it('should ignore links that start with `href=javascript`', function() {
+ fixture.innerHTML =
+ 'New PageClick Here>';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector('#skip-link');
+ assert.isTrue(axe.commons.dom.isSkipLink(node));
+ });
+});
diff --git a/test/integration/full/skip-link/skip-link-fail.html b/test/integration/full/skip-link/skip-link-fail.html
new file mode 100644
index 0000000000..c05b83d747
--- /dev/null
+++ b/test/integration/full/skip-link/skip-link-fail.html
@@ -0,0 +1,25 @@
+
+
+
+ skip-link test
+
+
+
+
+
+
+
+
+
+ bad link 1
+
+
+
+
+
\ No newline at end of file
diff --git a/test/integration/full/skip-link/skip-link-fail.js b/test/integration/full/skip-link/skip-link-fail.js
new file mode 100644
index 0000000000..57ffe7a8e1
--- /dev/null
+++ b/test/integration/full/skip-link/skip-link-fail.js
@@ -0,0 +1,41 @@
+describe('skip-link test pass', function() {
+ 'use strict';
+ var results;
+
+ before(function(done) {
+ axe.testUtils.awaitNestedLoad(function() {
+ axe.run({ runOnly: { type: 'rule', values: ['skip-link'] } }, function(
+ err,
+ r
+ ) {
+ assert.isNull(err);
+ results = r;
+ done();
+ });
+ });
+ });
+
+ describe('violations', function() {
+ it('should find 1', function() {
+ assert.lengthOf(results.violations, 1);
+ });
+
+ it('should find 1 nodes', function() {
+ assert.lengthOf(results.violations[0].nodes, 1);
+ });
+ });
+
+ describe('passes', function() {
+ it('should find 0', function() {
+ assert.lengthOf(results.passes, 0);
+ });
+ });
+
+ it('should find 0 inapplicable', function() {
+ assert.lengthOf(results.inapplicable, 0);
+ });
+
+ it('should find 0 incomplete', function() {
+ assert.lengthOf(results.incomplete, 0);
+ });
+});
diff --git a/test/integration/full/skip-link/skip-link-pass.html b/test/integration/full/skip-link/skip-link-pass.html
new file mode 100644
index 0000000000..dca2300bc4
--- /dev/null
+++ b/test/integration/full/skip-link/skip-link-pass.html
@@ -0,0 +1,42 @@
+
+
+
+ skip-link test
+
+
+
+
+
+
+
+
+
+
+
+ Link
+
+ Link
+
+
+ Link (angular)
+
+
+ Link
+
+
+
+
+ link
+ link
+
+
+
+
+
\ No newline at end of file
diff --git a/test/integration/full/skip-link/skip-link-pass.js b/test/integration/full/skip-link/skip-link-pass.js
new file mode 100644
index 0000000000..9db6232707
--- /dev/null
+++ b/test/integration/full/skip-link/skip-link-pass.js
@@ -0,0 +1,37 @@
+describe('skip-link test pass', function() {
+ 'use strict';
+ var results;
+
+ before(function(done) {
+ axe.testUtils.awaitNestedLoad(function() {
+ axe.run({ runOnly: { type: 'rule', values: ['skip-link'] } }, function(
+ err,
+ r
+ ) {
+ assert.isNull(err);
+ results = r;
+ done();
+ });
+ });
+ });
+
+ describe('violations', function() {
+ it('should find 0', function() {
+ assert.lengthOf(results.violations, 0);
+ });
+ });
+
+ describe('passes', function() {
+ it('should find 3', function() {
+ assert.lengthOf(results.passes[0].nodes, 3);
+ });
+ });
+
+ it('should find 0 inapplicable', function() {
+ assert.lengthOf(results.inapplicable, 0);
+ });
+
+ it('should find 1 incomplete', function() {
+ assert.lengthOf(results.incomplete, 1);
+ });
+});
diff --git a/test/integration/rules/skip-link/skip-link.html b/test/integration/rules/skip-link/skip-link.html
deleted file mode 100644
index 20abcba65f..0000000000
--- a/test/integration/rules/skip-link/skip-link.html
+++ /dev/null
@@ -1,13 +0,0 @@
-link
-link
-
-
-Link
-
-
-Link
-
-
-Link
-
-bad link 1
diff --git a/test/integration/rules/skip-link/skip-link.json b/test/integration/rules/skip-link/skip-link.json
deleted file mode 100644
index 302e81644d..0000000000
--- a/test/integration/rules/skip-link/skip-link.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "rule": "skip-link",
- "violations": [["#fail1"]],
- "incomplete": [["#canttell1"]],
- "passes": [["#pass1"], ["#pass2"]]
-}
diff --git a/test/rule-matches/skip-link-matches.js b/test/rule-matches/skip-link-matches.js
index 2e38e2a1a5..b06ec749f5 100644
--- a/test/rule-matches/skip-link-matches.js
+++ b/test/rule-matches/skip-link-matches.js
@@ -9,6 +9,7 @@ describe('skip-link-matches', function() {
return rule.id === 'skip-link';
});
link = document.createElement('a');
+ fixture.innerHTML = '';
});
afterEach(function() {
@@ -29,11 +30,6 @@ describe('skip-link-matches', function() {
assert.isFalse(rule.matches(link));
});
- it('returns true if the href attribute starts with #', function() {
- link.href = '#foo';
- assert.isTrue(rule.matches(link));
- });
-
it('returns false if the href attribute starts with #!', function() {
link.href = '#!foo';
assert.isFalse(rule.matches(link));
@@ -43,4 +39,14 @@ describe('skip-link-matches', function() {
link.href = '#/foo';
assert.isFalse(rule.matches(link));
});
+
+ it('returns true if the href attribute starts with #', function() {
+ link.href = '#main';
+ assert.isTrue(rule.matches(link));
+ });
+
+ it('returns true if the href attribute starts with /# (angular)', function() {
+ link.href = '/#main';
+ assert.isTrue(rule.matches(link));
+ });
});