// Source: https://www.w3.org/TR/html-aam-1.0/#element-mapping-table // Source: https://www.w3.org/TR/html-aria/ import getElementsByContentType from './get-elements-by-content-type'; import getGlobalAriaAttrs from './get-global-aria-attrs'; import arialabelledbyText from '../aria/arialabelledby-text'; import arialabelText from '../aria/arialabel-text'; 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'; import getExplicitRole from '../aria/get-explicit-role'; const sectioningElementSelector = getElementsByContentType('sectioning') .map(nodeName => `${nodeName}:not([role])`) .join(', ') + ' , main:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]'; // sectioning elements only have an accessible name if the // aria-label, aria-labelledby, or title attribute has valid // content. // can't go through the normal accessible name computation // as it leads into an infinite loop of asking for the role // of the element while the implicit role needs the name. // Source: https://www.w3.org/TR/html-aam-1.0/#section-and-grouping-element-accessible-name-computation // // form elements also follow this same pattern although not // specifically called out in the spec like section elements // (per Scott O'Hara) // Source: https://web-a11y.slack.com/archives/C042TSFGN/p1590607895241100?thread_ts=1590602189.217800&cid=C042TSFGN function hasAccessibleName(vNode) { // testing for when browsers give a
a region role: // chrome - always a region role // firefox - if non-empty aria-labelledby, aria-label, or title // safari - if non-empty aria-lablledby or aria-label // // we will go with safaris implantation as it is the least common // denominator const ariaLabelledby = sanitize(arialabelledbyText(vNode)); const ariaLabel = sanitize(arialabelText(vNode)); return !!(ariaLabelledby || ariaLabel); } const implicitHtmlRoles = { a: vNode => { return vNode.hasAttr('href') ? 'link' : null; }, area: vNode => { return vNode.hasAttr('href') ? 'link' : null; }, article: 'article', aside: 'complementary', body: 'document', button: 'button', datalist: 'listbox', dd: 'definition', dfn: 'term', details: 'group', dialog: 'dialog', dt: 'term', fieldset: 'group', figure: 'figure', footer: vNode => { const sectioningElement = closest(vNode, sectioningElementSelector); return !sectioningElement ? 'contentinfo' : null; }, form: vNode => { return hasAccessibleName(vNode) ? 'form' : null; }, h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading', header: vNode => { const sectioningElement = closest(vNode, sectioningElementSelector); return !sectioningElement ? 'banner' : null; }, hr: 'separator', img: vNode => { // 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 = getGlobalAriaAttrs().find(attr => vNode.hasAttr(attr) ); return emptyAlt && !hasGlobalAria && !isFocusable(vNode) ? 'presentation' : 'img'; }, input: vNode => { // Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element let suggestionsSourceElement; if (vNode.hasAttr('list')) { const listElement = idrefs(vNode.actualNode, 'list').filter( node => !!node )[0]; suggestionsSourceElement = listElement && listElement.nodeName.toLowerCase() === 'datalist'; } switch (vNode.props.type) { case 'checkbox': return 'checkbox'; case 'number': return 'spinbutton'; case 'radio': return 'radio'; case 'range': return 'slider'; case 'search': return !suggestionsSourceElement ? 'searchbox' : 'combobox'; case 'button': case 'image': case 'reset': case 'submit': return 'button'; case 'text': case 'tel': case 'url': case 'email': case '': return !suggestionsSourceElement ? 'textbox' : 'combobox'; default: return 'textbox'; } }, // 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', menu: 'list', nav: 'navigation', ol: 'list', optgroup: 'group', option: 'option', output: 'status', progress: 'progressbar', section: vNode => { return hasAccessibleName(vNode) ? 'region' : null; }, select: vNode => { return vNode.hasAttr('multiple') || parseInt(vNode.attr('size')) > 1 ? 'listbox' : 'combobox'; }, summary: 'button', table: 'table', tbody: 'rowgroup', td: vNode => { const table = closest(vNode, 'table'); const role = getExplicitRole(table); return ['grid', 'treegrid'].includes(role) ? 'gridcell' : 'cell'; }, textarea: 'textbox', tfoot: 'rowgroup', th: vNode => { if (isColumnHeader(vNode.actualNode)) { return 'columnheader'; } if (isRowHeader(vNode.actualNode)) { return 'rowheader'; } }, thead: 'rowgroup', tr: 'row', ul: 'list' }; export default implicitHtmlRoles;