From caccd381d3817cc46e824d1a76e5ea888bf519bf Mon Sep 17 00:00:00 2001 From: Cassey Lottman Date: Fri, 25 Jun 2021 11:16:39 -0500 Subject: [PATCH] fix(heading-order): use aria-level on headings in addition to role=header elements (#3028) * use aria-level on headings in addition to role=header elements * account for more cases * use heading level if aria-level is invalid on a hn tag * remove console log Co-authored-by: Wilco Fiers * update comment Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> * remove comment Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> * allow heading levels above 6 Co-authored-by: Wilco Fiers Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> --- .../navigation/heading-order-evaluate.js | 47 +++++--- test/checks/navigation/heading-order.js | 101 +++++++++++++++++- 2 files changed, 128 insertions(+), 20 deletions(-) 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

'