diff --git a/lib/checks/navigation/heading-order-evaluate.js b/lib/checks/navigation/heading-order-evaluate.js
index 09e055a392..4585e92ad1 100644
--- a/lib/checks/navigation/heading-order-evaluate.js
+++ b/lib/checks/navigation/heading-order-evaluate.js
@@ -1,26 +1,42 @@
import cache from '../../core/base/cache';
import { querySelectorAllFilter, getAncestry } from '../../core/utils';
import { isVisible } from '../../commons/dom';
+import { getRole } from '../../commons/aria';
function getLevel(vNode) {
- const role = vNode.attr('role');
- if (role && role.includes('heading')) {
- const ariaHeadingLevel = vNode.attr('aria-level');
- const level = parseInt(ariaHeadingLevel, 10);
-
- // default aria-level for a heading is 2 if it is
- // not set or set to an incorrect value
- // @see https://www.w3.org/TR/wai-aria-1.1/#heading
- if (isNaN(level) || level < 1 || level > 6) {
- return 2;
- }
+ const role = getRole(vNode);
+ const headingRole = role && role.includes('heading');
+ const ariaHeadingLevel = vNode.attr('aria-level');
+ const ariaLevel = parseInt(ariaHeadingLevel, 10);
+
+ const [, headingLevel] = vNode.props.nodeName.match(/h(\d)/) || [];
+
+ if (!headingRole) {
+ return -1;
+ }
- return level;
+ if (headingLevel && !ariaHeadingLevel) {
+ return parseInt(headingLevel, 10);
}
- const headingLevel = vNode.props.nodeName.match(/h(\d)/);
- if (headingLevel) {
- return parseInt(headingLevel[1], 10);
+ /*
+ * default aria-level for a role=heading is 2 if it is
+ * not set or set to an incorrect value.
+ * default aria-level for a heading element is the
+ * heading level.
+ * note that NVDA and VO allow any positive level
+ * @see https://www.w3.org/TR/wai-aria-1.1/#heading
+ * @see https://codepen.io/straker/pen/jOBjNNe
+ */
+ if (isNaN(ariaLevel) || ariaLevel < 1) {
+ if (headingLevel) {
+ return parseInt(headingLevel, 10);
+ }
+ return 2;
+ }
+
+ if (ariaLevel) {
+ return ariaLevel;
}
return -1;
@@ -49,7 +65,6 @@ function headingOrderEvaluate() {
level: getLevel(vNode)
};
});
-
this.data({ headingOrder });
cache.set('headingOrder', vNodes);
return true;
diff --git a/test/checks/navigation/heading-order.js b/test/checks/navigation/heading-order.js
index 04ca426dab..9d056573ed 100644
--- a/test/checks/navigation/heading-order.js
+++ b/test/checks/navigation/heading-order.js
@@ -33,7 +33,7 @@ describe('heading-order', function() {
it('should handle incorrect aria-level values', function() {
var vNode = queryFixture(
- '
One
Two
Three
'
+ 'One
Two
'
);
assert.isTrue(
axe.testUtils
@@ -49,10 +49,25 @@ describe('heading-order', function() {
{
ancestry: ['html > body > div:nth-child(1) > div:nth-child(2)'],
level: 2
- },
+ }
+ ]
+ });
+ });
+
+ it('should allow high aria-level values', function() {
+ var vNode = queryFixture(
+ 'One
'
+ );
+ assert.isTrue(
+ axe.testUtils
+ .getCheckEvaluate('heading-order')
+ .call(checkContext, null, {}, vNode, {})
+ );
+ assert.deepEqual(checkContext._data, {
+ headingOrder: [
{
- ancestry: ['html > body > div:nth-child(1) > div:nth-child(3)'],
- level: 2
+ ancestry: ['html > body > div:nth-child(1) > div'],
+ level: 12
}
]
});
@@ -79,6 +94,84 @@ describe('heading-order', function() {
});
});
+ it('should allow aria-level to override semantic level for hn tags and return true', function() {
+ var vNode = queryFixture(
+ 'Two
Four
'
+ );
+ assert.isTrue(
+ axe.testUtils
+ .getCheckEvaluate('heading-order')
+ .call(checkContext, null, {}, vNode, {})
+ );
+ assert.deepEqual(checkContext._data, {
+ headingOrder: [
+ {
+ ancestry: ['html > body > div:nth-child(1) > h1:nth-child(1)'],
+ level: 2
+ },
+ {
+ ancestry: ['html > body > div:nth-child(1) > h3:nth-child(2)'],
+ level: 4
+ }
+ ]
+ });
+ });
+
+ it('should ignore aria-level on iframe when not used with role=heading', function() {
+ var vNode = queryFixture('');
+ axe.testUtils
+ .getCheckEvaluate('heading-order')
+ .call(checkContext, null, {}, vNode, { initiator: true });
+ assert.deepEqual(checkContext._data, {
+ headingOrder: [
+ {
+ ancestry: ['html > body > div:nth-child(1) > iframe'],
+ level: -1
+ }
+ ]
+ });
+ });
+
+ it('should correctly give level on hn tag with role=heading', function() {
+ var vNode = queryFixture(
+ 'One
Three
'
+ );
+ assert.isTrue(
+ axe.testUtils
+ .getCheckEvaluate('heading-order')
+ .call(checkContext, null, {}, vNode, {})
+ );
+ assert.deepEqual(checkContext._data, {
+ headingOrder: [
+ {
+ ancestry: ['html > body > div:nth-child(1) > h1:nth-child(1)'],
+ level: 1
+ },
+ {
+ ancestry: ['html > body > div:nth-child(1) > h3:nth-child(2)'],
+ level: 3
+ }
+ ]
+ });
+ });
+
+ it('should return the heading level when an hn tag has an invalid aria-level', function() {
+ var vNode = queryFixture('One
');
+ assert.isTrue(
+ axe.testUtils
+ .getCheckEvaluate('heading-order')
+ .call(checkContext, null, {}, vNode, {})
+ );
+ assert.deepEqual(checkContext._data, {
+ headingOrder: [
+ {
+ ancestry: ['html > body > div:nth-child(1) > h1'],
+ level: 1
+ }
+ ]
+ });
+ });
+
it('should store the location of iframes', function() {
var vNode = queryFixture(
'One
Three
'