Skip to content

Commit

Permalink
feat: Use localName when available (#368)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Aug 8, 2020
1 parent 26519da commit 5db24b1
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-kangaroos-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"dom-accessibility-api": patch
---

Use `localName` to determine elements instead of `tagName`.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
// we only access it from a point where the library user can inject their
// own implementation. See https://github.com/eps1lon/dom-accessibility-api/blob/eb868428a31a093aecc531bf2dd17e8547bd0c3b/sources/accessible-name.ts#L33
"property": "getComputedStyle"
},
{
// tagName is oftentimes more expensive since it requires a toAsciiUpperCase of the local name.
// It certainly is in JSDOM: https://github.com/jsdom/jsdom/pull/3008
"property": "tagName",
"message": "Please use `getLocalName` instead because `tagName` is oftentimes more expensive since it requires a toAsciiUpperCase of the local name."
}
],
"no-restricted-syntax": [
Expand Down
17 changes: 9 additions & 8 deletions sources/accessible-name-and-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
isSVGSVGElement,
isSVGTitleElement,
queryIdRefs,
getLocalName,
} from "./util";

/**
Expand Down Expand Up @@ -220,16 +221,16 @@ function getTextualContent(declaration: CSSStyleDeclaration): string {
* @param element
*/
function isLabelableElement(element: Element): boolean {
const tagName = element.tagName;
const localName = getLocalName(element);

return (
tagName === "BUTTON" ||
(tagName === "INPUT" && element.getAttribute("type") !== "hidden") ||
tagName === "METER" ||
tagName === "OUTPUT" ||
tagName === "PROGRESS" ||
tagName === "SELECT" ||
tagName === "TEXTAREA"
localName === "button" ||
(localName === "input" && element.getAttribute("type") !== "hidden") ||
localName === "meter" ||
localName === "output" ||
localName === "progress" ||
localName === "select" ||
localName === "textarea"
);
}

Expand Down
106 changes: 54 additions & 52 deletions sources/getRole.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// https://w3c.github.io/html-aria/#document-conformance-requirements-for-use-of-aria-attributes-in-html

import { getLocalName } from "./util";

export default function getRole(element: Element): string | null {
const explicitRole = getExplicitRole(element);
if (explicitRole !== null) {
Expand All @@ -9,76 +11,76 @@ export default function getRole(element: Element): string | null {
return getImplicitRole(element);
}

const tagToRoleMappings: Record<string, string | undefined> = {
ARTICLE: "article",
ASIDE: "complementary",
BODY: "document",
BUTTON: "button",
DATALIST: "listbox",
DD: "definition",
DETAILS: "group",
DIALOG: "dialog",
DT: "term",
FIELDSET: "group",
FIGURE: "figure",
const localNameToRoleMappings: Record<string, string | undefined> = {
article: "article",
aside: "complementary",
body: "document",
button: "button",
datalist: "listbox",
dd: "definition",
details: "group",
dialog: "dialog",
dt: "term",
fieldset: "group",
figure: "figure",
// WARNING: Only with an accessible name
FORM: "form",
FOOTER: "contentinfo",
H1: "heading",
H2: "heading",
H3: "heading",
H4: "heading",
H5: "heading",
H6: "heading",
HEADER: "banner",
HR: "separator",
LEGEND: "legend",
LI: "listitem",
MATH: "math",
MAIN: "main",
MENU: "list",
NAV: "navigation",
OL: "list",
OPTGROUP: "group",
form: "form",
footer: "contentinfo",
h1: "heading",
h2: "heading",
h3: "heading",
h4: "heading",
h5: "heading",
h6: "heading",
header: "banner",
hr: "separator",
legend: "legend",
li: "listitem",
math: "math",
main: "main",
menu: "list",
nav: "navigation",
ol: "list",
optgroup: "group",
// WARNING: Only in certain context
OPTION: "option",
OUTPUT: "status",
PROGRESS: "progressbar",
option: "option",
output: "status",
progress: "progressbar",
// WARNING: Only with an accessible name
SECTION: "region",
SUMMARY: "button",
TABLE: "table",
TBODY: "rowgroup",
TEXTAREA: "textbox",
TFOOT: "rowgroup",
section: "region",
summary: "button",
table: "table",
tbody: "rowgroup",
textarea: "textbox",
tfoot: "rowgroup",
// WARNING: Only in certain context
TD: "cell",
TH: "columnheader",
THEAD: "rowgroup",
TR: "row",
UL: "list",
td: "cell",
th: "columnheader",
thead: "rowgroup",
tr: "row",
ul: "list",
};

function getImplicitRole(element: Element): string | null {
const mappedByTag = tagToRoleMappings[element.tagName];
const mappedByTag = localNameToRoleMappings[getLocalName(element)];
if (mappedByTag !== undefined) {
return mappedByTag;
}

switch (element.tagName) {
case "A":
case "AREA":
case "LINK":
switch (getLocalName(element)) {
case "a":
case "area":
case "link":
if (element.hasAttribute("href")) {
return "link";
}
break;
case "IMG":
case "img":
if ((element.getAttribute("alt") || "").length > 0) {
return "img";
}
break;
case "INPUT": {
case "input": {
const { type } = element as HTMLInputElement;
switch (type) {
case "button":
Expand Down Expand Up @@ -109,7 +111,7 @@ function getImplicitRole(element: Element): string | null {
return null;
}
}
case "SELECT":
case "select":
if (
element.hasAttribute("multiple") ||
(element as HTMLSelectElement).size > 1
Expand Down
30 changes: 21 additions & 9 deletions sources/util.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import getRole from "./getRole";

/**
* Safe Element.localName for all supported environments
* @param element
*/
export function getLocalName(element: Element): string {
return (
element.localName ??
// eslint-disable-next-line no-restricted-properties -- required for the fallback
element.tagName.toLowerCase()
);
}

export function isElement(node: Node | null): node is Element {
return node !== null && node.nodeType === node.ELEMENT_NODE;
}

export function isHTMLTableCaptionElement(
node: Node | null
): node is HTMLTableCaptionElement {
return isElement(node) && node.tagName === "CAPTION";
return isElement(node) && getLocalName(node) === "caption";
}

export function isHTMLInputElement(
node: Node | null
): node is HTMLInputElement {
return isElement(node) && node.tagName === "INPUT";
return isElement(node) && getLocalName(node) === "input";
}

export function isHTMLSelectElement(
node: Node | null
): node is HTMLSelectElement {
return isElement(node) && node.tagName === "SELECT";
return isElement(node) && getLocalName(node) === "select";
}

export function isHTMLTableElement(
node: Node | null
): node is HTMLTableElement {
return isElement(node) && node.tagName === "TABLE";
return isElement(node) && getLocalName(node) === "table";
}

export function isHTMLTextAreaElement(
node: Node | null
): node is HTMLTextAreaElement {
return isElement(node) && node.tagName === "TEXTAREA";
return isElement(node) && getLocalName(node) === "textarea";
}

export function safeWindow(node: Node): Window {
Expand All @@ -47,25 +59,25 @@ export function safeWindow(node: Node): Window {
export function isHTMLFieldSetElement(
node: Node | null
): node is HTMLFieldSetElement {
return isElement(node) && node.tagName === "FIELDSET";
return isElement(node) && getLocalName(node) === "fieldset";
}

export function isHTMLLegendElement(
node: Node | null
): node is HTMLLegendElement {
return isElement(node) && node.tagName === "LEGEND";
return isElement(node) && getLocalName(node) === "legend";
}

export function isSVGElement(node: Node | null): node is SVGElement {
return isElement(node) && (node as SVGElement).ownerSVGElement !== undefined;
}

export function isSVGSVGElement(node: Node | null): node is SVGSVGElement {
return isElement(node) && node.tagName === "svg";
return isElement(node) && getLocalName(node) === "svg";
}

export function isSVGTitleElement(node: Node | null): node is SVGTitleElement {
return isSVGElement(node) && node.tagName === "title";
return isSVGElement(node) && getLocalName(node) === "title";
}

/**
Expand Down

0 comments on commit 5db24b1

Please sign in to comment.