Created
November 24, 2017 15:45
-
-
Save lemnis/5f753515a34a8fc513bca6865c122330 to your computer and use it in GitHub Desktop.
Calculates which role should used by a screen reader following the HTML ARIA spec
This file contains hidden or 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
/** | |
* Follows https://www.w3.org/TR/2017/WD-html-aria-20171013/#docconformance | |
*/ | |
/** | |
* All aria roles | |
* @type {Array} | |
*/ | |
var roles = [ | |
"alert", "alertdialog", "application", "article", "banner", "button", "cell", | |
"checkbox", "columnheader", "combobox", "complementary", "contentinfo", | |
"definition", "dialog", "directory", "document", "feed", "figure", "form", | |
"grid", "gridcell", "group", "heading", "img", "link", "list", "listbox", | |
"listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", | |
"menuitemcheckbox", "menuitemradio", "navigation", "none", "note", "option", | |
"presentation", "progressbar", "radio", "radiogroup", "region", "row", | |
"rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", | |
"slider", "spinbutton", "status", "switch", "tab", "table", "tablist", | |
"tabpanel", "term", "textbox", "timer", "toolbar", "tooltip", "tree", | |
"treegrid", "treeitem", "command", "composite", "input", "landmark", "range", | |
"roletype", "section", "sectionhead", "select", "structure", "widget", "window" | |
]; | |
/** | |
* Stores info which is used in functions of rolePerHTMLTag, | |
* mostly a key as tagName with an array of allowed roles for that tag | |
* @type {Object} | |
*/ | |
var possibleRoles = { | |
"aWithHref": [ | |
"button", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio", | |
"option", "radio", "switch", "tab", "treeitem", "doc-backlink", | |
"doc-biblioref", "doc-glossref", "doc-noteref" | |
], | |
"article": [ | |
"feed", "presentation", "none", "document", "application", "main", "region" | |
], | |
"aside": [ | |
"feed", "note", "presentation", "none", "region", "search", "doc-example", | |
"doc-footnote", "doc-pullquote", "doc-tip" | |
], | |
"button": [ | |
"checkbox", "link", "menuitem", "menuitemcheckbox", "menuitemradio", | |
"option", "radio", "switch", "tab" | |
], | |
"dl": ["group", "presentation", "none", "doc-glossary"], | |
"embed": [ "application", "document", "presentation", "none", "img" ], | |
"figcaption": [ "group", "presentation", "none" ], | |
"fieldset": [ "group", "none", "presentation" ], | |
"footer": [ "group", "none", "presentation", "doc-footnote" ], | |
"form": [ "search", "none", "presentation" ], | |
"h1Toh6": [ "tab", "none", "presentation", "doc-subtitle" ], | |
"header": [ "group", "none", "presentation", "doc-footnote" ], | |
"hr": [ "presentation", "doc-pagebreak" ], | |
"iframe": [ "application", "document", "img" ], | |
"imgWithEmptyAlt": [ "presentation", "none" ], | |
"inputTypeButton": [ | |
"link, menuitem", "menuitemcheckbox", "menuitemradio", "radio", "switch", | |
"option", "tab" | |
], | |
"inputTypeImage": [ | |
"link", "menuitem", "menuitemcheckbox", "menuitemradio", "radio", "switch" | |
], | |
"inputTypeCheckbox": [ "menuitemcheckbox", "option", "switch" ], | |
"li": [ | |
"menuitem", "menuitemcheckbox", "menuitemradio", "option", "none", | |
"presentation", "radio", "separator", "tab", "treeitem", "doc-biblioentry", | |
"doc-endnote" | |
], | |
"nav": [ "doc-index", "doc-pagelist", "doc-toc" ], | |
"object": [ "application", "document", "img" ], | |
"section": [ | |
"alert", "alertdialog", "application", "banner", "complementary", | |
"contentinfo", "dialog", "document", "feed", "log", "main", "marquee", | |
"navigation", "none", "presentation", "search", "status", "tabpanel", | |
"doc-abstract", "doc-acknowledgments", "doc-afterword", "doc-appendix", | |
"doc-bibliography", "doc-chapter", "doc-colophon", "doc-conclusion", | |
"doc-credit", "doc-credits", "doc-dedication", "doc-endnotes", "doc-epilogue", | |
"doc-errata", "doc-example", "doc-foreword", "doc-index", "doc-introduction", | |
"doc-notice", "doc-pagelist", "doc-part", "doc-preface", "doc-prologue", | |
"doc-pullquote", "doc-qna", "doc-toc" | |
], | |
"svg": [ "application", "document", "img" ], | |
"ul": [ | |
"directory", "group", "listbox", "menu", "menubar", "radiogroup", | |
"tablist", "toolbar", "tree", "presentation" | |
] | |
} | |
/** | |
* Contains a function for each htmlTag where not all roles allowed | |
* @type {Object} | |
*/ | |
var rolePerHTMLTag = { | |
a: (el, role) => { | |
if(el.href) { | |
return possibleRoles.aWithHref.indexOf(role) > -1 ? role : "link"; | |
} else { | |
return role; | |
} | |
}, | |
area: (el, role) => { | |
if(el.href) return role ? null : "link"; | |
return role; | |
}, | |
article: (el, role) => possibleRoles.article.indexOf(role) > -1 ? role : "article", | |
aside: (el, role) => possibleRoles.aside.indexOf(role) > -1 ? role : "complementary", | |
audio: (el, role) => role == "application" ? "application" : null, | |
base: () => null, | |
body: (el, role) => "document", | |
button: (el, role) => { | |
if(el.type == "menu") { | |
return role == "menuitem" ? "menuitem" : "button"; | |
} | |
return possibleRoles.button.indexOf(role) > -1 ? role : "button"; | |
}, | |
caption: () => null, | |
col: () => null, | |
colgroup: () => null, | |
datalist: () => "listbox", | |
dd: () => "definition", | |
details: () => "group", | |
dialog: (el, role) => role == "alertdialog" ? "alertdialog" : "dialog", | |
dl: (el, role) => possibleRoles.dl.indexOf(role) > -1 ? role : "list", | |
dt: () => "listitem", | |
embed: (el, role) => possibleRoles.embed.indexOf(role) > -1 ? role : null, | |
figcaption: (el, role) => possibleRoles.figcaption.indexOf(role) > -1 ? role : null, | |
fieldset: (el, role) => possibleRoles.fieldset.indexOf(role) > -1 ? role : null, | |
figure: (el, role) => possibleRoles.figure.indexOf(role) > -1 ? role : "figure", | |
footer: (el, role) => { | |
let hasImplicitContentinfoRole = !getParentWithTagName(el, ["ARTICLE", "ASIDE", "MAIN", "NAV", "SECTION"]); | |
let hasAllowedRole = possibleRoles.footer.indexOf(role) > -1; | |
if(hasAllowedRole){ | |
return role; | |
} else if (hasImplicitContentinfoRole) { | |
return "contentinfo"; | |
} else { | |
return null; | |
} | |
}, | |
form: (el, role) => possibleRoles.form.indexOf(role) > -1 ? role : "form", | |
h1: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
h2: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
h3: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
h4: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
h5: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
h6: (el, role) => possibleRoles.h1Toh6.indexOf(role) > -1 ? role : "heading", | |
head: () => null, | |
header: (el, role) => { | |
let hasImplicitBannerRole = !getParentWithTagName(el, ["ARTICLE", "ASIDE", "MAIN", "NAV", "SECTION"]); | |
let hasAllowedRole = possibleRoles.header.indexOf(role) > -1; | |
if(hasAllowedRole){ | |
return role; | |
} else if (hasImplicitBannerRole) { | |
return "banner"; | |
} else { | |
return null; | |
} | |
}, | |
hr: (el, role) => possibleRoles.hr.indexOf(role) > -1 ? role : "seperator", | |
html: () => null, | |
iframe: (el, role) => possibleRoles.iframe.indexOf(role) > -1 ? role : null, | |
img: (el, role) => { | |
var hasAllowedEmptyAltRole = possibleRoles.imgWithAlt.indexOf(role) > -1 | |
if(el.alt) { | |
// any role exept the roles used by empty alt values | |
return hasAllowedEmptyAltRole ? "img" : role; | |
} else { | |
return hasAllowedEmptyAltRole ? role : null; | |
} | |
}, | |
input: (el, role) => { | |
switch(el.type) { | |
case "button": | |
return possibleRoles.inputTypeButton.indexOf(role) > -1 ? role : "button"; | |
case "checkbox": | |
if(role == "button" && el.hasAttribute("aria-pressed")) return "button"; | |
return possibleRoles.inputTypeCheckbox.indexOf(role) > -1 ? role : "checkbox"; | |
case "image": | |
return possibleRoles.inputTypeImage.indexOf(role) > -1 ? role : "button"; | |
case "number": | |
return "spinbutton"; | |
case "radio": | |
return role == "menuitemradio" ? "menuitemradio" : "radio"; | |
case "range": | |
return "slider"; | |
case "search": | |
return el.list ? "combobox" : "searchbox"; | |
case "reset": | |
case "submit": | |
return "button"; | |
case "email": | |
case "tel": | |
case "text": | |
case "url": | |
return el.list ? "combobox" : "textbox"; | |
default: | |
return null; | |
} | |
}, | |
keygen: () => null, | |
label: () => null, | |
legend: () => null, | |
li: (el, role) => { | |
let hasImplicitListitemRole = getParentWithTagName(el, ["OL", "UL"]); | |
if(hasImplicitListitemRole) { | |
return possibleRoles.li.indexOf(role) > -1 ? role : "listitem"; | |
} else { | |
return null; | |
} | |
}, | |
link: (el, role) => { | |
if(el.href) return role ? null : "link"; | |
return role; | |
}, | |
main: () => "main", | |
map: () => null, | |
math: () => "math", | |
menu: (el, role) => el.type == "context" ? "menu" : role, | |
menuitem: (el, role) => { | |
switch (el.type) { | |
case "command": | |
return "menuitem"; | |
case "checkbox": | |
return "menuitemcheckbox"; | |
case "radio": | |
return "menuitemradio"; | |
default: | |
return role; | |
} | |
}, | |
meta: () => null, | |
meter: () => null, | |
nav: (el, role) => possibleRoles.nav.indexOf(role) > -1 ? role : "navigation", | |
noscript: () => null, | |
object: (el, role) => possibleRoles.object.indexOf(role) > -1 ? role : null, | |
"ol": "list", | |
optgroup: () => "group", | |
option: (el, role) => { | |
let withinOptionList = ["select", "optgroup", "datalist"].indexOf(el.parentNode); | |
return withinOptionList ? "option" : null; | |
}, | |
output: (el, role) => role ? role : "status", | |
param: () => null, | |
picture: () => null, | |
progress: (el, role) => "progressbar", | |
script: () => null, | |
section: (el, role) => { | |
let hasValidRole = possibleRoles.section.indexOf(role) > -1; | |
if(hasValidRole) return role; | |
// only if accessible name | |
if(el.title || el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby")){ | |
return "section"; | |
} else { | |
return null; | |
} | |
}, | |
select: (el, role) => { | |
if(el.multiple && el.size > 1){ | |
return "listbox"; | |
} else if(!el.multiple && el.size <= 1) { | |
return role == "menu" ? role : "combobox"; | |
} | |
return role; | |
}, | |
source: () => null, | |
style: () => null, | |
svg: (el, role) => possibleRoles.svg.indexOf(role) > -1 ? role : null, | |
summary: () => "button", | |
table: (el, role) => role ? role : "table", | |
template: () => null, | |
textarea: () => "textbox", | |
thead: (el, role) => role ? role : "rowgroup", | |
tbody: (el, role) => role ? role : "rowgroup", | |
tfoot: (el, role) => role ? role : "rowgroup", | |
title: () => null, | |
td: (el, role) => getParentWithTagName(el, ["TABLE"]) ? "cell" : role, | |
th: () => getParentWithTagName(el, ["THEAD"]) ? "columnheader" : "rowheader", | |
tr: (el, role) => { | |
// role=row, may be explicitly declared when child of a table element with role=grid | |
return role ? role : "row"; | |
}, | |
track: () => null, | |
ul: (el, role) => possibleRoles.ul.indexOf(role) > -1 ? role : "list", | |
video: (el, role) => role == "application" ? "application" : null | |
}; | |
/** | |
* Finds nearest parent with a specifig tagName | |
* @param {HTMLElement} el child - starting pointer | |
* @param {Array<String>} tagName Array containg capatilized tagnames | |
* @return {HTMLElement} Parent that matches one of the tagnames | |
*/ | |
function getParentWithTagName(el, tagName){ | |
while (el.parentNode){ | |
if(tagName.indexOf(el.tagName) > -1) return el; | |
el = el.parentNode | |
} | |
} | |
export function getComputedRole(el) { | |
var role = el.getAttribute("role"); | |
// check if given role exist | |
if(role) role = roles.indexOf(role) > - 1 ? role : null; | |
var tagName = el.tagName.toLowerCase(); | |
// call possible custom function if tag has any | |
if (rolePerHTMLTag[tagName]) return rolePerHTMLTag[tagName](el, role); | |
// default behavior a.k.a. set role | |
return role; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment