From c180e708a7b6b0392681b7fec030434710b23d9f Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 8 Jun 2020 10:51:50 -0600 Subject: [PATCH 01/11] feat(get-role): add presentation role resolution and inheritance --- lib/commons/aria/get-role.js | 102 +++++++++++++- lib/commons/aria/lookup-table.js | 2 +- lib/commons/text/title-text.js | 10 +- test/commons/aria/attributes.js | 17 +-- test/commons/aria/get-role.js | 135 +++++++++++++++++++ test/commons/aria/implicit-role.js | 4 +- test/commons/text/form-control-value.js | 7 - test/commons/text/native-text-alternative.js | 4 +- 8 files changed, 246 insertions(+), 35 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 030ea47683..6bd9234b2a 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -1,5 +1,84 @@ import isValidRole from './is-valid-role'; import getImplicitRole from './implicit-role'; +import lookupTable from './lookup-table'; +import { getNodeFromTree, closest } from '../../core/utils'; +import isFocusable from '../dom/is-focusable'; +import sanitize from '../text/sanitize'; + +const inheritsPresentation = { + dd: ['dl'], + dt: ['dl'], + li: ['ul', 'ol'], + tbody: ['table'], + td: ['table'], + tfoot: ['table'], + th: ['table'], + thead: ['table'], + tr: ['table'] +}; + +function hasGlobalAriaAttributes(vNode) { + return lookupTable.globalAttributes.find( + attr => vNode.hasAttr(attr) && sanitize(vNode.attr(attr)) + ); +} + +function resolveImplicitRole(vNode) { + const implicitRole = getImplicitRole(vNode.actualNode); + + if (implicitRole) { + // an images role is considered implicitly presentation if the + // alt attribute is empty, but that shouldn't be the case if it + // has global aria attributes or is focusable, so we need to + // override the role back to `img` + // e.g. + if ( + vNode.props.nodeName === 'img' && + (hasGlobalAriaAttributes(vNode) || isFocusable(vNode.actualNode)) + ) { + return 'img'; + } + + // role presentation inheritance. + // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none + // + // when an element inherits the presentational role from a parent + // is not defined in the spec, but through testing it seems to be + // when a specific HTML parent relationship is required and that + // parent has `role=presentation`, then the child inherits the + // role (i.e. table, ul, dl) + // + // from Scott O'Hara: + // + // "the expectation for me, in standard html is that element + // structures that require specific parent/child relationships, + // if the parent is set to presentational that should set the + // children to presentational. ala, tables and lists." + // "but outside of those specific constructs, i would not expect + // role=presentation to do anything to child element roles" + if (inheritsPresentation[vNode.props.nodeName]) { + const ancestorVNode = closest(vNode, '[role]'); + + if (!ancestorVNode) { + return implicitRole; + } + + const allowedParents = inheritsPresentation[vNode.props.nodeName]; + + if (!allowedParents.includes(ancestorVNode.props.nodeName)) { + return implicitRole; + } + + const ancestorRole = getRole(ancestorVNode); + + if (['presentation', 'none'].includes(ancestorRole)) { + return ancestorRole; + } + } + } + + return implicitRole; +} /** * Return the accessible role of an element @@ -17,6 +96,9 @@ import getImplicitRole from './implicit-role'; */ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { node = node.actualNode || node; + + const vNode = getNodeFromTree(node); + if (node.nodeType !== 1) { return null; } @@ -32,14 +114,26 @@ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { return isValidRole(role, { allowAbstract: abstracts }); }); - const explicitRole = validRoles[0]; + const explicitRole = validRoles[0] || null; + if (noImplicit) { + return explicitRole; + } // Get the implicit role, if permitted - if (!explicitRole && !noImplicit) { - return getImplicitRole(node); + const implicitRole = resolveImplicitRole(vNode); + + // role conflict resolution + // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none + // See also: https://github.com/w3c/aria/issues/1270 + if ( + !explicitRole || + (['presentation', 'none'].includes(explicitRole) && + (hasGlobalAriaAttributes(vNode) || isFocusable(node))) + ) { + return implicitRole; } - return explicitRole || null; + return explicitRole; } export default getRole; diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js index c73997b5a7..2d72248de5 100644 --- a/lib/commons/aria/lookup-table.js +++ b/lib/commons/aria/lookup-table.js @@ -2168,7 +2168,7 @@ lookupTable.implicitHtmlRole = { }, hr: 'separator', img: vNode => { - return vNode.hasAttr('alt') && !vNode.attr('alt') ? null : 'img'; + return vNode.hasAttr('alt') && !vNode.attr('alt') ? 'presentation' : 'img'; }, input: vNode => { // Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element diff --git a/lib/commons/text/title-text.js b/lib/commons/text/title-text.js index 0b40535348..ab9d7b6c32 100644 --- a/lib/commons/text/title-text.js +++ b/lib/commons/text/title-text.js @@ -1,15 +1,7 @@ import matches from '../matches/matches'; import getRole from '../aria/get-role'; -const alwaysTitleElements = [ - 'button', - 'iframe', - 'a[href]', - { - nodeName: 'input', - properties: { type: 'button' } - } -]; +const alwaysTitleElements = ['iframe']; /** * Get title text diff --git a/test/commons/aria/attributes.js b/test/commons/aria/attributes.js index bbcba380d7..191178eb34 100644 --- a/test/commons/aria/attributes.js +++ b/test/commons/aria/attributes.js @@ -33,16 +33,19 @@ describe('aria.allowedAttr', function() { 'use strict'; var orig; + var origGlobals; beforeEach(function() { orig = axe.commons.aria.lookupTable.role; + origGlobals = axe.commons.aria.lookupTable.globalAttributes; }); afterEach(function() { axe.commons.aria.lookupTable.role = orig; + axe.commons.aria.lookupTable.globalAttributes = origGlobals; }); it('should returned the attributes property for the proper role', function() { - var orig = (axe.commons.aria.lookupTable.globalAttributes = ['world']); + axe.commons.aria.lookupTable.globalAttributes = ['world']; axe.commons.aria.lookupTable.role = { cats: { attributes: { @@ -52,11 +55,10 @@ describe('aria.allowedAttr', function() { }; assert.deepEqual(axe.commons.aria.allowedAttr('cats'), ['hello', 'world']); - axe.commons.aria.lookupTable.globalAttributes = orig; }); it('should also check required attributes', function() { - var orig = (axe.commons.aria.lookupTable.globalAttributes = ['world']); + axe.commons.aria.lookupTable.globalAttributes = ['world']; axe.commons.aria.lookupTable.role = { cats: { attributes: { @@ -71,18 +73,13 @@ describe('aria.allowedAttr', function() { 'world', 'hello' ]); - axe.commons.aria.lookupTable.globalAttributes = orig; }); it('should return an array with globally allowed attributes', function() { - var result, - orig = (axe.commons.aria.lookupTable.globalAttributes = ['world']); - + axe.commons.aria.lookupTable.globalAttributes = ['world']; axe.commons.aria.lookupTable.role = {}; - result = axe.commons.aria.allowedAttr('cats'); - assert.deepEqual(result, ['world']); - axe.commons.aria.lookupTable.globalAttributes = orig; + assert.deepEqual(axe.commons.aria.allowedAttr('cats'), ['world']); }); }); diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index daeb97994a..b006c972e0 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -3,6 +3,7 @@ describe('aria.getRole', function() { var aria = axe.commons.aria; var roleDefinitions = aria.lookupTable.role; var flatTreeSetup = axe.testUtils.flatTreeSetup; + var fixture = document.querySelector('#fixture'); var orig; beforeEach(function() { @@ -10,6 +11,7 @@ describe('aria.getRole', function() { }); afterEach(function() { + fixture.innerHTML = ''; axe.commons.aria.lookupTable.role = orig; }); @@ -74,6 +76,131 @@ describe('aria.getRole', function() { assert.isNull(aria.getRole(node)); }); + it('runs role resolution with role=none', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'none'); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'listitem'); + }); + + it('runs role resolution with role=presentation', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'presentation'); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'listitem'); + }); + + it('returns as img', function() { + var node = document.createElement('img'); + node.setAttribute('alt', ''); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'img'); + }); + + it('returns as img', function() { + var node = document.createElement('img'); + node.setAttribute('role', 'presentation'); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'img'); + }); + + it('returns as img', function() { + var node = document.createElement('img'); + node.setAttribute('role', 'none'); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'img'); + }); + + it('handles focusable element with role="none"', function() { + var node = document.createElement('button'); + node.setAttribute('role', 'none'); + flatTreeSetup(node); + assert.equal(aria.getRole(node), 'button'); + }); + + it('handles presentation role inheritance for ul', function() { + fixture.innerHTML = ''; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for ol', function() { + fixture.innerHTML = '
  1. foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for dt', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for dd', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for thead', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for td', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for th', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for thead', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for tr', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('does not override explicit role with presentation role inheritance', function() { + fixture.innerHTML = + ''; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'listitem'); + }); + describe('noImplicit', function() { it('returns the implicit role by default', function() { var node = document.createElement('li'); @@ -87,6 +214,14 @@ describe('aria.getRole', function() { assert.isNull(aria.getRole(node, { noImplicit: true })); }); + it('does not do role resolution if noImplicit: true', function() { + var node = document.createElement('li'); + node.setAttribute('role', 'none'); + node.setAttribute('aria-label', 'foo'); + flatTreeSetup(node); + assert.equal(aria.getRole(node, { noImplicit: true }), 'none'); + }); + it('still returns the explicit role', function() { var node = document.createElement('li'); node.setAttribute('role', 'button'); diff --git a/test/commons/aria/implicit-role.js b/test/commons/aria/implicit-role.js index 41ef00ff2a..37f1a0750d 100644 --- a/test/commons/aria/implicit-role.js +++ b/test/commons/aria/implicit-role.js @@ -159,11 +159,11 @@ describe('aria.implicitRole', function() { assert.equal(implicitRole(node), 'img'); }); - it('should return null for "img" with empty alt', function() { + it('should return presentation for "img" with empty alt', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); flatTreeSetup(fixture); - assert.isNull(implicitRole(node)); + assert.equal(implicitRole(node), 'presentation'); }); it('should return button for "input[type=button]"', function() { diff --git a/test/commons/text/form-control-value.js b/test/commons/text/form-control-value.js index d32138b132..4fcfe0e514 100644 --- a/test/commons/text/form-control-value.js +++ b/test/commons/text/form-control-value.js @@ -28,13 +28,6 @@ describe('text.formControlValue', function() { assert.equal(formControlValue(target, { startNode: target }), ''); }); - it('returns `` when the role is not supposed to return a value', function() { - var target = queryFixture( - '' - ); - assert.equal(formControlValue(target), ''); - }); - it('returns `` when accessibleNameFromFieldValue says the role is unsupported', function() { var target = queryFixture( '' diff --git a/test/commons/text/native-text-alternative.js b/test/commons/text/native-text-alternative.js index b0e3a3dd48..c08f5f545b 100644 --- a/test/commons/text/native-text-alternative.js +++ b/test/commons/text/native-text-alternative.js @@ -35,13 +35,13 @@ describe('text.nativeTextAlternative', function() { it('returns `` when the element has role=presentation', function() { var vNode = queryFixture( - '' + 'foo' ); assert.equal(nativeTextAlternative(vNode), ''); }); it('returns `` when the element has role=none', function() { - var vNode = queryFixture(''); + var vNode = queryFixture('foo'); assert.equal(nativeTextAlternative(vNode), ''); }); }); From c0d9fbddd470b0411347bc94a15068fbabd44f0a Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 8 Jun 2020 10:57:21 -0600 Subject: [PATCH 02/11] comment --- lib/commons/aria/get-role.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 6bd9234b2a..c1fe2d96cd 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -5,6 +5,20 @@ import { getNodeFromTree, closest } from '../../core/utils'; import isFocusable from '../dom/is-focusable'; import sanitize from '../text/sanitize'; +// when an element inherits the presentational role from a parent +// is not defined in the spec, but through testing it seems to be +// when a specific HTML parent relationship is required and that +// parent has `role=presentation`, then the child inherits the +// role (i.e. table, ul, dl) +// +// from Scott O'Hara: +// +// "the expectation for me, in standard html is that element +// structures that require specific parent/child relationships, +// if the parent is set to presentational that should set the +// children to presentational. ala, tables and lists." +// "but outside of those specific constructs, i would not expect +// role=presentation to do anything to child element roles" const inheritsPresentation = { dd: ['dl'], dt: ['dl'], @@ -41,21 +55,6 @@ function resolveImplicitRole(vNode) { // role presentation inheritance. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none - // - // when an element inherits the presentational role from a parent - // is not defined in the spec, but through testing it seems to be - // when a specific HTML parent relationship is required and that - // parent has `role=presentation`, then the child inherits the - // role (i.e. table, ul, dl) - // - // from Scott O'Hara: - // - // "the expectation for me, in standard html is that element - // structures that require specific parent/child relationships, - // if the parent is set to presentational that should set the - // children to presentational. ala, tables and lists." - // "but outside of those specific constructs, i would not expect - // role=presentation to do anything to child element roles" if (inheritsPresentation[vNode.props.nodeName]) { const ancestorVNode = closest(vNode, '[role]'); From 8cc43b9149bf85759585ffc47cd1e425ccb26f3a Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 8 Jun 2020 11:06:18 -0600 Subject: [PATCH 03/11] add test --- test/commons/aria/get-role.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index b006c972e0..7290d1cf6c 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -92,7 +92,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('returns as img', function() { + it('returns with global attribute as img', function() { var node = document.createElement('img'); node.setAttribute('alt', ''); node.setAttribute('aria-label', 'foo'); @@ -100,7 +100,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'img'); }); - it('returns as img', function() { + it('returns with global attribute as img', function() { var node = document.createElement('img'); node.setAttribute('role', 'presentation'); node.setAttribute('aria-label', 'foo'); @@ -108,7 +108,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'img'); }); - it('returns as img', function() { + it('returns with global attribute as img', function() { var node = document.createElement('img'); node.setAttribute('role', 'none'); node.setAttribute('aria-label', 'foo'); @@ -193,6 +193,14 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); + it('returns implicit role for presentation role inheritance if ancestor is not the required ancestor', function() { + fixture.innerHTML = + '
  • foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'listitem'); + }); + it('does not override explicit role with presentation role inheritance', function() { fixture.innerHTML = '
  • foo
'; From e67e77bf20f9c99f3943bd9cf7cb29b9b50a8d0d Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 11 Jun 2020 11:59:25 -0600 Subject: [PATCH 04/11] fix all but test --- lib/commons/aria/get-role.js | 125 ++++++++------- lib/commons/aria/lookup-table.js | 18 ++- test/checks/aria/aria-allowed-role.js | 11 ++ test/commons/aria/get-role.js | 210 +++++++++++++------------ test/commons/aria/implicit-role.js | 14 ++ test/commons/forms/is-aria-combobox.js | 3 + test/commons/forms/is-aria-listbox.js | 4 + test/commons/forms/is-aria-range.js | 4 + test/commons/forms/is-aria-textbox.js | 4 + 9 files changed, 237 insertions(+), 156 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index c1fe2d96cd..7d88cf1e6b 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -1,15 +1,20 @@ import isValidRole from './is-valid-role'; import getImplicitRole from './implicit-role'; import lookupTable from './lookup-table'; -import { getNodeFromTree, closest } from '../../core/utils'; +import { getNodeFromTree } from '../../core/utils'; import isFocusable from '../dom/is-focusable'; -import sanitize from '../text/sanitize'; // when an element inherits the presentational role from a parent // is not defined in the spec, but through testing it seems to be // when a specific HTML parent relationship is required and that // parent has `role=presentation`, then the child inherits the -// role (i.e. table, ul, dl) +// role (i.e. table, ul, dl). Further testing has shown that +// intermediate elements (such as divs) break this chain only in +// Chrome. +// +// Also, any nested structure chains reset the role (so two nested +// lists with the topmost list role=none will not cause the nested +// list to inherit the role=none). // // from Scott O'Hara: // @@ -19,61 +24,63 @@ import sanitize from '../text/sanitize'; // children to presentational. ala, tables and lists." // "but outside of those specific constructs, i would not expect // role=presentation to do anything to child element roles" -const inheritsPresentation = { - dd: ['dl'], - dt: ['dl'], - li: ['ul', 'ol'], +const inheritsPresentationChain = { + // valid parent elements, any other element will prevent any + // children from inheriting a presentational role from a valid + // ancestor + td: ['tr'], + th: ['tr'], + tr: ['thead', 'tbody', 'tfoot', 'table'], + thead: ['table'], tbody: ['table'], - td: ['table'], tfoot: ['table'], - th: ['table'], - thead: ['table'], - tr: ['table'] + table: [], + li: ['ol', 'ul'], + ol: [], + ul: [], + dt: ['dl'], + dd: ['dl'], + dl: [] }; -function hasGlobalAriaAttributes(vNode) { - return lookupTable.globalAttributes.find( - attr => vNode.hasAttr(attr) && sanitize(vNode.attr(attr)) - ); -} +// role presentation inheritance. +// Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none +function getPresentationalAncestorRole(vNode) { + const chain = inheritsPresentationChain[vNode.props.nodeName]; -function resolveImplicitRole(vNode) { - const implicitRole = getImplicitRole(vNode.actualNode); + if (!chain) { + return null; + } - if (implicitRole) { - // an images role is considered implicitly presentation if the - // alt attribute is empty, but that shouldn't be the case if it - // has global aria attributes or is focusable, so we need to - // override the role back to `img` - // e.g. - if ( - vNode.props.nodeName === 'img' && - (hasGlobalAriaAttributes(vNode) || isFocusable(vNode.actualNode)) - ) { - return 'img'; + const role = getRole(vNode, { noImplicit: true }); + if (role) { + if (['presentation', 'none'].includes(role)) { + return role; } - // role presentation inheritance. - // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none - if (inheritsPresentation[vNode.props.nodeName]) { - const ancestorVNode = closest(vNode, '[role]'); + // an explicit role of anything other than presentational will + // prevent any children from inheriting a presentational role + // from a valid ancestor + return null; + } - if (!ancestorVNode) { - return implicitRole; - } + if (vNode.parent && chain.includes(vNode.parent.props.nodeName)) { + return getPresentationalAncestorRole(vNode.parent); + } - const allowedParents = inheritsPresentation[vNode.props.nodeName]; + return null; +} - if (!allowedParents.includes(ancestorVNode.props.nodeName)) { - return implicitRole; - } +function resolveImplicitRole(vNode) { + const implicitRole = getImplicitRole(vNode.actualNode); - const ancestorRole = getRole(ancestorVNode); + if (!implicitRole) { + return null; + } - if (['presentation', 'none'].includes(ancestorRole)) { - return ancestorRole; - } - } + const presentationalRole = getPresentationalAncestorRole(vNode); + if (presentationalRole) { + return presentationalRole; } return implicitRole; @@ -95,7 +102,6 @@ function resolveImplicitRole(vNode) { */ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { node = node.actualNode || node; - const vNode = getNodeFromTree(node); if (node.nodeType !== 1) { @@ -114,22 +120,29 @@ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { }); const explicitRole = validRoles[0] || null; - if (noImplicit) { + + if (explicitRole && !['presentation', 'none'].includes(explicitRole)) { return explicitRole; } - // Get the implicit role, if permitted - const implicitRole = resolveImplicitRole(vNode); - // role conflict resolution + // note: Chrome returns a list with resolved role as "generic" + // instead of as a list + // (e.g.
  • hello
) + // we will return it as a list as that is the best option. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none // See also: https://github.com/w3c/aria/issues/1270 - if ( - !explicitRole || - (['presentation', 'none'].includes(explicitRole) && - (hasGlobalAriaAttributes(vNode) || isFocusable(node))) - ) { - return implicitRole; + const hasGlobalAria = lookupTable.globalAttributes.find(attr => + vNode.hasAttr(attr) + ); + if (hasGlobalAria || isFocusable(node)) { + // return null if there is a conflict resolution but no implicit + // has been set as the explicit role is not the true role + return noImplicit ? null : resolveImplicitRole(vNode); + } + + if (!noImplicit && !explicitRole) { + return resolveImplicitRole(vNode); } return explicitRole; diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js index 2d72248de5..199a201a22 100644 --- a/lib/commons/aria/lookup-table.js +++ b/lib/commons/aria/lookup-table.js @@ -4,6 +4,7 @@ import idrefs from '../dom/idrefs'; import isColumnHeader from '../table/is-column-header'; import isRowHeader from '../table/is-row-header'; import sanitize from '../text/sanitize'; +import isFocusable from '../dom/is-focusable'; import { closest } from '../../core/utils'; const isNull = value => value === null; @@ -2168,7 +2169,19 @@ lookupTable.implicitHtmlRole = { }, hr: 'separator', img: vNode => { - return vNode.hasAttr('alt') && !vNode.attr('alt') ? 'presentation' : 'img'; + // an images role is considered implicitly presentation if the + // alt attribute is empty. But that shouldn't be the case if it + // has global aria attributes or is focusable, so we need to + // override the role back to `img` + // e.g. + const emptyAlt = vNode.hasAttr('alt') && !vNode.attr('alt'); + const hasGlobalAria = lookupTable.globalAttributes.find(attr => + vNode.hasAttr(attr) + ); + + return emptyAlt && !hasGlobalAria && !isFocusable(vNode.actualNode) + ? 'presentation' + : 'img'; }, input: vNode => { // Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element @@ -2202,6 +2215,9 @@ lookupTable.implicitHtmlRole = { return !suggestionsSourceElement ? 'searchbox' : 'combobox'; } }, + // Note: if an li (or some other elms) do not have a required + // parent, Firefox ignores the implicit semantic role and treats + // it as a generic text. li: 'listitem', main: 'main', math: 'math', diff --git a/test/checks/aria/aria-allowed-role.js b/test/checks/aria/aria-allowed-role.js index 357a1cb143..eb2ae66569 100644 --- a/test/checks/aria/aria-allowed-role.js +++ b/test/checks/aria/aria-allowed-role.js @@ -10,6 +10,17 @@ describe('aria-allowed-role', function() { checkContext.reset(); }); + it('tests', function() { + fixture.innerHTML = + ''; + var node = fixture.firstChild; + flatTreeSetup(fixture); + var actual = axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, node); + assert.isTrue(actual); + }); + it('returns true if given element is an ignoredTag in options', function() { var node = document.createElement('article'); node.setAttribute('role', 'presentation'); diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 7290d1cf6c..5fbea03ab3 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -92,30 +92,6 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('returns with global attribute as img', function() { - var node = document.createElement('img'); - node.setAttribute('alt', ''); - node.setAttribute('aria-label', 'foo'); - flatTreeSetup(node); - assert.equal(aria.getRole(node), 'img'); - }); - - it('returns with global attribute as img', function() { - var node = document.createElement('img'); - node.setAttribute('role', 'presentation'); - node.setAttribute('aria-label', 'foo'); - flatTreeSetup(node); - assert.equal(aria.getRole(node), 'img'); - }); - - it('returns with global attribute as img', function() { - var node = document.createElement('img'); - node.setAttribute('role', 'none'); - node.setAttribute('aria-label', 'foo'); - flatTreeSetup(node); - assert.equal(aria.getRole(node), 'img'); - }); - it('handles focusable element with role="none"', function() { var node = document.createElement('button'); node.setAttribute('role', 'none'); @@ -123,90 +99,126 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'button'); }); - it('handles presentation role inheritance for ul', function() { - fixture.innerHTML = '
  • foo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + describe('presentational role inheritance', function() { + it('handles presentation role inheritance for ul', function() { + fixture.innerHTML = + '
  • foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for ol', function() { - fixture.innerHTML = '
  1. foo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for ol', function() { + fixture.innerHTML = + '
  1. foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for dt', function() { - fixture.innerHTML = - '
foo
bar>
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for dt', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for dd', function() { - fixture.innerHTML = - '
foo
bar>
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for dd', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for thead', function() { - fixture.innerHTML = - '
higoodbye
hifoo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for thead', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for td', function() { - fixture.innerHTML = - '
higoodbye
hifoo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for td', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for th', function() { - fixture.innerHTML = - '
higoodbye
hifoo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for th', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for thead', function() { - fixture.innerHTML = - '
higoodbye
hifoo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for tbody', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('handles presentation role inheritance for tr', function() { - fixture.innerHTML = - '
higoodbye
hifoo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'presentation'); - }); + it('handles presentation role inheritance for tr', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('returns implicit role for presentation role inheritance if ancestor is not the required ancestor', function() { - fixture.innerHTML = - '
  • foo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'listitem'); - }); + it('handles presentation role inheritance for tfoot', function() { + fixture.innerHTML = + '
higoodbye
hifoo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); - it('does not override explicit role with presentation role inheritance', function() { - fixture.innerHTML = - '
  • foo
'; - flatTreeSetup(fixture); - var node = fixture.querySelector('#target'); - assert.equal(aria.getRole(node), 'listitem'); + it('returns implicit role for presentation role inheritance if ancestor is not the required ancestor', function() { + fixture.innerHTML = + '
  • foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'listitem'); + }); + + it('does not override explicit role with presentation role inheritance', function() { + fixture.innerHTML = + '
  • foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'listitem'); + }); + + it('does not continue presentation role with explicit role in between', function() { + fixture.innerHTML = + '
foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'cell'); + }); + + it('handles presentation role inheritance with invalid role in between', function() { + fixture.innerHTML = + '
foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('does not continue presentation role through nested layers', function() { + fixture.innerHTML = + '
    • foo
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'listitem'); + }); }); describe('noImplicit', function() { @@ -227,7 +239,7 @@ describe('aria.getRole', function() { node.setAttribute('role', 'none'); node.setAttribute('aria-label', 'foo'); flatTreeSetup(node); - assert.equal(aria.getRole(node, { noImplicit: true }), 'none'); + assert.equal(aria.getRole(node, { noImplicit: true }), null); }); it('still returns the explicit role', function() { diff --git a/test/commons/aria/implicit-role.js b/test/commons/aria/implicit-role.js index 37f1a0750d..613ec74ed2 100644 --- a/test/commons/aria/implicit-role.js +++ b/test/commons/aria/implicit-role.js @@ -166,6 +166,20 @@ describe('aria.implicitRole', function() { assert.equal(implicitRole(node), 'presentation'); }); + it('should return img for "img" with empty alt and global aria attribute', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'img'); + }); + + it('should return img for "img" with empty alt and focusable', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'img'); + }); + it('should return button for "input[type=button]"', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); diff --git a/test/commons/forms/is-aria-combobox.js b/test/commons/forms/is-aria-combobox.js index ff1a2c6c8e..fb039995a6 100644 --- a/test/commons/forms/is-aria-combobox.js +++ b/test/commons/forms/is-aria-combobox.js @@ -5,17 +5,20 @@ describe('forms.isAriaCombobox', function() { it('returns true for an element with role=combobox', function() { var node = document.createElement('div'); node.setAttribute('role', 'combobox'); + axe.utils.getFlattenedTree(node); assert.isTrue(isAriaCombobox(node)); }); it('returns false for elements without role', function() { var node = document.createElement('div'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaCombobox(node)); }); it('returns false for elements with incorrect role', function() { var node = document.createElement('div'); node.setAttribute('role', 'main'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaCombobox(node)); }); }); diff --git a/test/commons/forms/is-aria-listbox.js b/test/commons/forms/is-aria-listbox.js index 1da9fa0e20..1fd62ccf2f 100644 --- a/test/commons/forms/is-aria-listbox.js +++ b/test/commons/forms/is-aria-listbox.js @@ -5,22 +5,26 @@ describe('forms.isAriaListbox', function() { it('returns true for an element with role=listbox', function() { var node = document.createElement('div'); node.setAttribute('role', 'listbox'); + axe.utils.getFlattenedTree(node); assert.isTrue(isAriaListbox(node)); }); it('returns false for elements without role', function() { var node = document.createElement('div'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaListbox(node)); }); it('returns false for elements with incorrect role', function() { var node = document.createElement('div'); node.setAttribute('role', 'main'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaListbox(node)); }); it('returns false for native select', function() { var node = document.createElement('select'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaListbox(node)); }); }); diff --git a/test/commons/forms/is-aria-range.js b/test/commons/forms/is-aria-range.js index 1322cd14c1..9f1aabecb0 100644 --- a/test/commons/forms/is-aria-range.js +++ b/test/commons/forms/is-aria-range.js @@ -8,6 +8,7 @@ describe('forms.isAriaRange', function() { var node = document.createElement('div'); node.setAttribute('role', role); node.setAttribute('aria-valuenow', '0'); + axe.utils.getFlattenedTree(node); assert.isTrue( isAriaRange(node), 'role="' + role + '" is not an aria range role' @@ -17,12 +18,14 @@ describe('forms.isAriaRange', function() { it('returns false for elements without role', function() { var node = document.createElement('div'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaRange(node)); }); it('returns false for elements with incorrect role', function() { var node = document.createElement('div'); node.setAttribute('role', 'main'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaRange(node)); }); @@ -45,6 +48,7 @@ describe('forms.isAriaRange', function() { if (elm.type) { node.setAttribute('type', elm.type); } + axe.utils.getFlattenedTree(node); assert.isFalse( isAriaRange(node), node.outterHTML + ' is not an aria range element' diff --git a/test/commons/forms/is-aria-textbox.js b/test/commons/forms/is-aria-textbox.js index 047cec9117..1ae717e5ad 100644 --- a/test/commons/forms/is-aria-textbox.js +++ b/test/commons/forms/is-aria-textbox.js @@ -5,23 +5,27 @@ describe('forms.isAriaTextbox', function() { it('returns true for an element with role=textbox', function() { var node = document.createElement('div'); node.setAttribute('role', 'textbox'); + axe.utils.getFlattenedTree(node); assert.isTrue(isAriaTextbox(node)); }); it('returns false for elements without role', function() { var node = document.createElement('div'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaTextbox(node)); }); it('returns false for elements with incorrect role', function() { var node = document.createElement('div'); node.setAttribute('role', 'main'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaTextbox(node)); }); it('returns false for native textbox inputs', function() { var node = document.createElement('input'); node.setAttribute('type', 'text'); + axe.utils.getFlattenedTree(node); assert.isFalse(isAriaTextbox(node)); }); }); From 13a0c17238b4bb7840f3a748104decdc5310eb0e Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 17 Jun 2020 08:50:25 -0600 Subject: [PATCH 05/11] Update lib/commons/aria/get-role.js Co-authored-by: Wilco Fiers --- lib/commons/aria/get-role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 823c2711bd..81c7e1ea6f 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -123,7 +123,7 @@ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { // we will return it as a list as that is the best option. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none // See also: https://github.com/w3c/aria/issues/1270 - const hasGlobalAria = lookupTable.globalAttributes.find(attr => + const hasGlobalAria = lookupTable.globalAttributes.some(attr => vNode.hasAttr(attr) ); if (hasGlobalAria || isFocusable(node)) { From 2eb6719693282fd3586e84f002956d1cd52af1f8 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 17 Jun 2020 08:52:25 -0600 Subject: [PATCH 06/11] Update lib/commons/aria/get-role.js Co-authored-by: Wilco Fiers --- lib/commons/aria/get-role.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 81c7e1ea6f..d8da1600ff 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -132,8 +132,8 @@ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { return noImplicit ? null : resolveImplicitRole(vNode); } - if (!noImplicit && !explicitRole) { - return resolveImplicitRole(vNode); + if (noImplicit && !explicitRole) { + return null; } return explicitRole; From 3761d7e27fea5b17d23eb03ec0c3a2022d9c28dc Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 17 Jun 2020 08:52:34 -0600 Subject: [PATCH 07/11] Update lib/commons/aria/get-role.js Co-authored-by: Wilco Fiers --- lib/commons/aria/get-role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index d8da1600ff..5de513a883 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -53,7 +53,7 @@ function getPresentationalAncestorRole(vNode) { return null; } - const role = getRole(vNode, { noImplicit: true }); + const role = getExplicitRole(vNode); if (role) { if (['presentation', 'none'].includes(role)) { return role; From 628ee6eb429213bc0b579382c42e76d22bab8428 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 17 Jun 2020 08:53:40 -0600 Subject: [PATCH 08/11] Update lib/commons/aria/get-role.js Co-authored-by: Wilco Fiers --- lib/commons/aria/get-role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 5de513a883..59082da962 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -136,7 +136,7 @@ function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { return null; } - return explicitRole; + return resolveImplicitRole(vNode); } export default getRole; From a31f62d028b869b02a5a90e164bdb1cdefb8608c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 17 Jun 2020 10:23:23 -0600 Subject: [PATCH 09/11] fixes --- lib/commons/aria/get-role.js | 77 ++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 59082da962..5aa73dbc25 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -46,40 +46,39 @@ const inheritsPresentationChain = { // role presentation inheritance. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none -function getPresentationalAncestorRole(vNode) { +function getPresentationalAncestorRole(vNode, explicitRoleOptions) { const chain = inheritsPresentationChain[vNode.props.nodeName]; if (!chain) { return null; } - const role = getExplicitRole(vNode); - if (role) { - if (['presentation', 'none'].includes(role)) { - return role; - } - + const role = getExplicitRole(vNode, explicitRoleOptions); + if (role && !hasConflictResolution(vNode)) { // an explicit role of anything other than presentational will // prevent any children from inheriting a presentational role // from a valid ancestor - return null; + return ['presentation', 'none'].includes(role) ? role : null; } - if (vNode.parent && chain.includes(vNode.parent.props.nodeName)) { - return getPresentationalAncestorRole(vNode.parent); + if (!vNode.parent || !chain.includes(vNode.parent.props.nodeName)) { + return null; } - return null; + return getPresentationalAncestorRole(vNode.parent); } -function resolveImplicitRole(vNode) { - const implicitRole = getImplicitRole(vNode.actualNode); +function resolveImplicitRole(vNode, explicitRoleOptions) { + const implicitRole = getImplicitRole(vNode); if (!implicitRole) { return null; } - const presentationalRole = getPresentationalAncestorRole(vNode); + const presentationalRole = getPresentationalAncestorRole( + vNode, + explicitRoleOptions + ); if (presentationalRole) { return presentationalRole; } @@ -87,6 +86,20 @@ function resolveImplicitRole(vNode) { return implicitRole; } +// role conflict resolution +// note: Chrome returns a list with resolved role as "generic" +// instead of as a list +// (e.g.
  • hello
) +// we will return it as a list as that is the best option. +// Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none +// See also: https://github.com/w3c/aria/issues/1270 +function hasConflictResolution(vNode) { + const hasGlobalAria = lookupTable.globalAttributes.some(attr => + vNode.hasAttr(attr) + ); + return hasGlobalAria || isFocusable(vNode.actualNode); +} + /** * Return the semantic role of an element. * @@ -103,40 +116,34 @@ function resolveImplicitRole(vNode) { * * @deprecated noImplicit option is deprecated. Use aria.getExplicitRole instead. */ -function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) { +function getRole(node, { noImplicit, ...explicitRoleOptions } = {}) { const vNode = node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); - node = vNode.actualNode; if (vNode.props.nodeType !== 1) { return null; } - const explicitRole = getExplicitRole(vNode, { fallback, abstracts, dpub }); - if (explicitRole && !['presentation', 'none'].includes(explicitRole)) { + const explicitRole = getExplicitRole(vNode, explicitRoleOptions); + const implicitRole = noImplicit + ? null + : resolveImplicitRole(vNode, explicitRoleOptions); + + if (!explicitRole) { + return implicitRole; + } + + if (!['presentation', 'none'].includes(explicitRole)) { return explicitRole; } - // role conflict resolution - // note: Chrome returns a list with resolved role as "generic" - // instead of as a list - // (e.g.
  • hello
) - // we will return it as a list as that is the best option. - // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none - // See also: https://github.com/w3c/aria/issues/1270 - const hasGlobalAria = lookupTable.globalAttributes.some(attr => - vNode.hasAttr(attr) - ); - if (hasGlobalAria || isFocusable(node)) { + if (hasConflictResolution(vNode)) { // return null if there is a conflict resolution but no implicit // has been set as the explicit role is not the true role - return noImplicit ? null : resolveImplicitRole(vNode); - } - - if (noImplicit && !explicitRole) { - return null; + return implicitRole; } - return resolveImplicitRole(vNode); + // role presentation or none and no conflict resolution + return explicitRole; } export default getRole; From e1d8250d341a7d7a9365c3515e56b44a5b8fece7 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Wed, 17 Jun 2020 10:43:28 -0600 Subject: [PATCH 10/11] throw error --- lib/commons/aria/get-role.js | 10 +++++- test/commons/aria/get-role.js | 61 +++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 5aa73dbc25..04d315ade0 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -61,7 +61,15 @@ function getPresentationalAncestorRole(vNode, explicitRoleOptions) { return ['presentation', 'none'].includes(role) ? role : null; } - if (!vNode.parent || !chain.includes(vNode.parent.props.nodeName)) { + // if we can't look at the parent then we can't know if the node + // inherits the presentational role or not + if (!vNode.parent) { + throw new ReferenceError( + 'Cannot determine role presentational inheritance of a required parent outside the current scope.' + ); + } + + if (!chain.includes(vNode.parent.props.nodeName)) { return null; } diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 849a708b40..843f6d1853 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -50,9 +50,9 @@ describe('aria.getRole', function() { }); it('returns the implicit role if the explicit is invalid', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'foobar'); - flatTreeSetup(node); + fixture.innerHTML = '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); @@ -77,18 +77,18 @@ describe('aria.getRole', function() { }); it('runs role resolution with role=none', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'none'); - node.setAttribute('aria-label', 'foo'); - flatTreeSetup(node); + fixture.innerHTML = + '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); it('runs role resolution with role=presentation', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'presentation'); - node.setAttribute('aria-label', 'foo'); - flatTreeSetup(node); + fixture.innerHTML = + '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); @@ -219,12 +219,23 @@ describe('aria.getRole', function() { var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); + + it('throws an error if the tree is incomplete', function() { + fixture.innerHTML = + '
  • foo
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(node); + assert.throws(function() { + aria.getRole(node); + }); + }); }); describe('noImplicit', function() { it('returns the implicit role by default', function() { - var node = document.createElement('li'); - flatTreeSetup(node); + fixture.innerHTML = '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); @@ -250,17 +261,18 @@ describe('aria.getRole', function() { }); it('returns the implicit role with `noImplicit: false`', function() { - var node = document.createElement('li'); - flatTreeSetup(node); + fixture.innerHTML = '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node, { noImplicit: false }), 'listitem'); }); }); describe('abstracts', function() { it('ignores abstract roles by default', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'section'); - flatTreeSetup(node); + fixture.innerHTML = '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(roleDefinitions.section.type, 'abstract'); assert.equal(aria.getRole(node), 'listitem'); }); @@ -274,9 +286,9 @@ describe('aria.getRole', function() { }); it('does not returns abstract roles with `abstracts: false`', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'section'); - flatTreeSetup(node); + fixture.innerHTML = '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(roleDefinitions.section.type, 'abstract'); assert.equal(aria.getRole(node, { abstracts: false }), 'listitem'); }); @@ -328,9 +340,10 @@ describe('aria.getRole', function() { }); it('respects the defaults', function() { - var node = document.createElement('li'); - node.setAttribute('role', 'doc-chapter section'); - flatTreeSetup(node); + fixture.innerHTML = + '
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node, { fallback: true }), 'listitem'); }); From 35fff9303cab582f3f60b94591910268a86affca Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 22 Jun 2020 12:05:14 -0600 Subject: [PATCH 11/11] changes --- lib/commons/aria/get-role.js | 58 +++++++++++++++++------------------ test/commons/aria/get-role.js | 16 ++++++++++ 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index 04d315ade0..e2af68102c 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -35,32 +35,22 @@ const inheritsPresentationChain = { thead: ['table'], tbody: ['table'], tfoot: ['table'], - table: [], li: ['ol', 'ul'], - ol: [], - ul: [], - dt: ['dl'], - dd: ['dl'], - dl: [] + // dts and dds can be wrapped in divs and the div will pass through + // the presentation role + dt: ['dl', 'div'], + dd: ['dl', 'div'], + div: ['dl'] }; // role presentation inheritance. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none -function getPresentationalAncestorRole(vNode, explicitRoleOptions) { - const chain = inheritsPresentationChain[vNode.props.nodeName]; - - if (!chain) { +function getInheritedRole(vNode, explicitRoleOptions) { + const parentNodeNames = inheritsPresentationChain[vNode.props.nodeName]; + if (!parentNodeNames) { return null; } - const role = getExplicitRole(vNode, explicitRoleOptions); - if (role && !hasConflictResolution(vNode)) { - // an explicit role of anything other than presentational will - // prevent any children from inheriting a presentational role - // from a valid ancestor - return ['presentation', 'none'].includes(role) ? role : null; - } - // if we can't look at the parent then we can't know if the node // inherits the presentational role or not if (!vNode.parent) { @@ -69,11 +59,27 @@ function getPresentationalAncestorRole(vNode, explicitRoleOptions) { ); } - if (!chain.includes(vNode.parent.props.nodeName)) { + // parent is not a valid ancestor that can inherit presentation + if (!parentNodeNames.includes(vNode.parent.props.nodeName)) { return null; } - return getPresentationalAncestorRole(vNode.parent); + const parentRole = getExplicitRole(vNode.parent, explicitRoleOptions); + if ( + ['none', 'presentation'].includes(parentRole) && + !hasConflictResolution(vNode.parent) + ) { + return parentRole; + } + + // an explicit role of anything other than presentational will + // prevent any children from inheriting a presentational role + // from a valid ancestor + if (parentRole) { + return null; + } + + return getInheritedRole(vNode.parent, explicitRoleOptions); } function resolveImplicitRole(vNode, explicitRoleOptions) { @@ -83,10 +89,7 @@ function resolveImplicitRole(vNode, explicitRoleOptions) { return null; } - const presentationalRole = getPresentationalAncestorRole( - vNode, - explicitRoleOptions - ); + const presentationalRole = getInheritedRole(vNode, explicitRoleOptions); if (presentationalRole) { return presentationalRole; } @@ -132,12 +135,9 @@ function getRole(node, { noImplicit, ...explicitRoleOptions } = {}) { } const explicitRole = getExplicitRole(vNode, explicitRoleOptions); - const implicitRole = noImplicit - ? null - : resolveImplicitRole(vNode, explicitRoleOptions); if (!explicitRole) { - return implicitRole; + return noImplicit ? null : resolveImplicitRole(vNode, explicitRoleOptions); } if (!['presentation', 'none'].includes(explicitRole)) { @@ -147,7 +147,7 @@ function getRole(node, { noImplicit, ...explicitRoleOptions } = {}) { if (hasConflictResolution(vNode)) { // return null if there is a conflict resolution but no implicit // has been set as the explicit role is not the true role - return implicitRole; + return noImplicit ? null : resolveImplicitRole(vNode, explicitRoleOptions); } // role presentation or none and no conflict resolution diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 843f6d1853..317520babd 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -132,6 +132,22 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); + it('handles presentation role inheritance for dt with div wrapper', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + + it('handles presentation role inheritance for dd with div wrapper', function() { + fixture.innerHTML = + '
foo
bar>
'; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); + assert.equal(aria.getRole(node), 'presentation'); + }); + it('handles presentation role inheritance for thead', function() { fixture.innerHTML = '
higoodbye
hifoo
';