-
Notifications
You must be signed in to change notification settings - Fork 755
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(standards): add get-elements-by-content-type and implicit-html-r…
…oles (#2375) * feat(standards): add get-elements-by-content-type and implicit-html-roles * cleanup * lookuptable to use implicit roles from commons/standards
- Loading branch information
Showing
6 changed files
with
272 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import standards from '../../standards'; | ||
|
||
/** | ||
* Return a list of html elements whose content type matches the provided value. Note: this will not work for 'interactive' content types as those depend on the element. | ||
* @param {String} type The desired content type | ||
* @return {String[]} List of all elements matching the type | ||
*/ | ||
function getElementsByContentType(type) { | ||
return Object.keys(standards.htmlElms).filter(nodeName => { | ||
const elm = standards.htmlElms[nodeName]; | ||
|
||
if (elm.contentTypes) { | ||
return elm.contentTypes.includes(type); | ||
} | ||
|
||
// some elements do not have content types | ||
if (!elm.variant) { | ||
return false; | ||
} | ||
|
||
if (elm.variant.default && elm.variant.default.contentTypes) { | ||
return elm.variant.default.contentTypes.includes(type); | ||
} | ||
|
||
// content type depends on a virtual node | ||
return false; | ||
}); | ||
} | ||
|
||
export default getElementsByContentType; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// 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'; | ||
|
||
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 <section> 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. <img alt="" aria-label="foo"></img> | ||
const emptyAlt = vNode.hasAttr('alt') && !vNode.attr('alt'); | ||
const hasGlobalAria = getGlobalAriaAttrs().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 | ||
const listElement = idrefs(vNode.actualNode, 'list').filter( | ||
node => !!node | ||
)[0]; | ||
const suggestionsSourceElement = | ||
listElement && listElement.nodeName.toLowerCase() === 'datalist'; | ||
|
||
switch ((vNode.attr('type') || '').toLowerCase()) { | ||
case 'button': | ||
case 'image': | ||
case 'reset': | ||
case 'submit': | ||
return 'button'; | ||
case 'checkbox': | ||
return 'checkbox'; | ||
case 'email': | ||
case 'tel': | ||
case 'text': | ||
case 'url': | ||
case '': // text is the default value | ||
return !suggestionsSourceElement ? 'textbox' : 'combobox'; | ||
case 'number': | ||
return 'spinbutton'; | ||
case 'radio': | ||
return 'radio'; | ||
case 'range': | ||
return 'slider'; | ||
case 'search': | ||
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', | ||
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: '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; |
Oops, something went wrong.