diff --git a/.changeset/tricky-socks-build.md b/.changeset/tricky-socks-build.md new file mode 100644 index 00000000..48723d8e --- /dev/null +++ b/.changeset/tricky-socks-build.md @@ -0,0 +1,5 @@ +--- +"dom-accessibility-api": minor +--- + +feat: refresh mappings to latest editor's draft diff --git a/sources/__tests__/accessible-name.js b/sources/__tests__/accessible-name.js index 3d01488c..83e888b7 100644 --- a/sources/__tests__/accessible-name.js +++ b/sources/__tests__/accessible-name.js @@ -63,6 +63,11 @@ afterEach(cleanup); describe("to upstream", () => { // name from content test.each([ + [ + "button", + `
latin a
`, + "latin a", + ], [ "cell", `
greek alpha
`, @@ -78,6 +83,11 @@ describe("to upstream", () => { `
greek gamma
`, "greek gamma", ], + [ + "comment", + `
latin b
`, + "latin b", + ], [ "gridcell", `
greek delta
`, @@ -88,6 +98,12 @@ describe("to upstream", () => { `
greek zeta
`, "greek zeta", ], + [ + "heading", + `
latin c
`, + "latin c", + ], + ["link", `
latin d
`, "latin d"], [ "menuitem", `
  • greek eta
  • `, @@ -283,14 +299,14 @@ describe("slots", () => { test.each([ [ ` -
    I reference my name
    +I reference my name `, "I'm prohibited a name", ], [ ` -
    I reference my name
    +I reference my name
    I'm prohibited a name
    `, "I'm prohibited a name", diff --git a/sources/__tests__/getRole.js b/sources/__tests__/getRole.js index cb5b958d..cd346f15 100644 --- a/sources/__tests__/getRole.js +++ b/sources/__tests__/getRole.js @@ -21,10 +21,17 @@ it("uses the first role", () => { expect(getRole(element)).toBe("textbox"); }); -it("ignores empty roles", () => { +it("ignores empty roles when have implicit role", () => { const element = document.createElement("div"); element.setAttribute("role", " "); + expect(getRole(element)).toBe("generic"); +}); + +it("ignores empty roles when have no implicit role", () => { + const element = document.createElement("abbr"); + element.setAttribute("role", " "); + expect(getRole(element)).toBeNull(); }); @@ -43,34 +50,47 @@ function createElementFactory(tagName, attributes) { // prettier-ignore const cases = [ ["a element with a href", "link", createElementFactory("a", { href: "any" })], - ["a element without a href", null, createElementFactory("a", {})], + ["a element without a href", "generic", createElementFactory("a", {})], ["abbr", null, createElementFactory("abbr", {})], - ["address", null, createElementFactory("address", {})], + ["address", "group", createElementFactory("address", {})], ["area with a href", "link", createElementFactory("area", { href: "any" })], - ["area without a href", null, createElementFactory("area", {})], + ["area without a href", "generic", createElementFactory("area", {})], ["article", "article", createElementFactory("article", {})], + // WARNING: Only in certain context ["aside", "complementary", createElementFactory("aside", {})], ["audio", null, createElementFactory("audio", {})], + ["b", "generic", createElementFactory("b", {})], ["base", null, createElementFactory("base", {})], - ["blockquote", null, createElementFactory("blockquote", {})], - ["body", null, createElementFactory("body", {})], + ["bdi", "generic", createElementFactory("bdi", {})], + ["bdo", "generic", createElementFactory("bdo", {})], + ["blockquote", "blockquote", createElementFactory("blockquote", {})], + ["body", "generic", createElementFactory("body", {})], + ["br", null, createElementFactory("br", {})], ["button", "button", createElementFactory("button", {})], ["canvas", null, createElementFactory("canvas", {})], - ["caption", null, createElementFactory("caption", {})], + ["caption", "caption", createElementFactory("caption", {})], + ["cite", null, createElementFactory("cite", {})], + ["code", "code", createElementFactory("code", {})], ["col", null, createElementFactory("col", {})], ["colgroup", null, createElementFactory("colgroup", {})], + ["data", "generic", createElementFactory("data", {})], ["datalist", "listbox", createElementFactory("datalist", {})], - ["dd", "definition", createElementFactory("dd", {})], - ["del", null, createElementFactory("del", {})], + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aam/pull/376 + ["dd", null, createElementFactory("dd", {})], + ["del", "deletion", createElementFactory("del", {})], ["details", "group", createElementFactory("details", {})], + ["dfn", "term", createElementFactory("dfn", {})], ["dialog", "dialog", createElementFactory("dialog", {})], - ["div", null, createElementFactory("div", {})], + ["div", "generic", createElementFactory("div", {})], ["dl", null, createElementFactory("dl", {})], - ["dt", "term", createElementFactory("dt", {})], - ["em", null, createElementFactory("em", {})], + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aam/pull/376 + ["dt", null, createElementFactory("dt", {})], + ["em", "emphasis", createElementFactory("em", {})], ["embed", null, createElementFactory("embed", {})], - ["figcaption", null, createElementFactory("figcaption", {})], ["fieldset", "group", createElementFactory("fieldset", {})], + ["figcaption", null, createElementFactory("figcaption", {})], ["figure", "figure", createElementFactory("figure", {})], // WARNING: Only in certain context ["footer", "contentinfo", createElementFactory("footer", {})], @@ -84,9 +104,12 @@ const cases = [ ["h6", "heading", createElementFactory("h6", {})], // WARNING: Only in certain context ["header", "banner", createElementFactory("header", {})], - ["hgroup", null, createElementFactory("hgroup", {})], + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aria/issues/451 + ["hgroup", "group", createElementFactory("hgroup", {})], ["hr", "separator", createElementFactory("hr", {})], ["html", "document", createElementFactory("html", {})], + ["i", "generic", createElementFactory("i", {})], ["iframe", null, createElementFactory("iframe", {})], ["img with alt=\"some text\"", "img", createElementFactory("img", {alt: "text"})], ["img with missing alt", "img", createElementFactory("img", {})], @@ -117,18 +140,20 @@ const cases = [ ["input type=time", null, createElementFactory("input", {type: "time"})], ["input type=url", "textbox", createElementFactory("input", {type: "url"})], ["input type=week", null, createElementFactory("input", {type: "week"})], - ["ins", null, createElementFactory("ins", {})], + ["ins", "insertion", createElementFactory("ins", {})], + ["kbd", null, createElementFactory("kbd", {})], ["label", null, createElementFactory("label", {})], - ["legend", 'legend', createElementFactory("legend", {})], + ["legend", null, createElementFactory("legend", {})], // WARNING: Only in certain context ["li", "listitem", createElementFactory("li", {})], - ["link element with a href", "link", createElementFactory("link", {href: "some"})], + ["link", null, createElementFactory("link", {})], ["main", "main", createElementFactory("main", {})], ["map", null, createElementFactory("map", {})], + ["mark", null, createElementFactory("mark", {})], ["math", "math", createElementFactory("math", {})], ["menu", "list", createElementFactory("menu", {})], ["meta", null, createElementFactory("meta", {})], - ["meter", null, createElementFactory("meter", {})], + ["meter", "meter", createElementFactory("meter", {})], ["nav", "navigation", createElementFactory("nav", {})], ["noscript", null, createElementFactory("noscript", {})], ["object", null, createElementFactory("object", {})], @@ -137,12 +162,21 @@ const cases = [ // Warning: Only in certain context ["option", "option", createElementFactory("option", {})], ["output", "status", createElementFactory("output", {})], - ["p", null, createElementFactory("p", {})], + ["p", "paragraph", createElementFactory("p", {})], ["param", null, createElementFactory("param", {})], ["picture", null, createElementFactory("picture", {})], - ["pre", null, createElementFactory("pre", {})], + ["pre", "generic", createElementFactory("pre", {})], ["progress", "progressbar", createElementFactory("progress", {})], + ["q", "generic", createElementFactory("q", {})], + ["rp", null, createElementFactory("rp", {})], + ["rt", null, createElementFactory("rt", {})], + ["ruby", null, createElementFactory("ruby", {})], + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aria/issues/466 + ["s", "deletion", createElementFactory("s", {})], + ["samp", "generic", createElementFactory("samp", {})], ["script", null, createElementFactory("script", {})], + ["search", "search", createElementFactory("search", {})], // WARNING: Only with a name ["section", "region", createElementFactory("section", {})], ["select, no multiple, no size", "combobox", createElementFactory("select", {})], @@ -150,36 +184,41 @@ const cases = [ ["select, size greater 1", "listbox", createElementFactory("select", {size: 2})], ["select, multiple", "listbox", createElementFactory("select", {multiple: true})], ["slot", null, createElementFactory("slot", {})], + ["small", "generic", createElementFactory("small", {})], ["source", null, createElementFactory("source", {})], - ["span", null, createElementFactory("span", {})], - ["strong", null, createElementFactory("strong", {})], + ["span", "generic", createElementFactory("span", {})], + ["strong", "strong", createElementFactory("strong", {})], ["style", null, createElementFactory("style", {})], - ["svg", null, createElementFactory("svg", {})], - ["sub", null, createElementFactory("sub", {})], + ["sub", "subscript", createElementFactory("sub", {})], ["summary", "button", createElementFactory("summary", {})], - ["sup", null, createElementFactory("sup", {})], + ["sup", "superscript", createElementFactory("sup", {})], + ["svg", "graphics-document", createElementFactory("svg", {})], ["table", "table", createElementFactory("table", {})], ["tbody", "rowgroup", createElementFactory("tbody", {})], + // WARNING: Only in certain contexts + ["td", "cell", createElementFactory("td", {})], ["template", null, createElementFactory("template", {})], ["textarea", "textbox", createElementFactory("textarea", {})], ["tfoot", "rowgroup", createElementFactory("tfoot", {})], + // WARNING: Only in certain context + ["th", "columnheader", createElementFactory("th", {})], ["thead", "rowgroup", createElementFactory("thead", {})], - ["time", null, createElementFactory("time", {})], + ["time", "time", createElementFactory("time", {})], ["title", null, createElementFactory("title", {})], - // WARNING: Only in certain contexts - ["td", "cell", createElementFactory("td", {})], - ["th", "columnheader", createElementFactory("th", {})], ["tr", "row", createElementFactory("tr", {})], ["track", null, createElementFactory("track", {})], + ["u", "generic", createElementFactory("u", {})], ["ul", "list", createElementFactory("ul", {})], + ["var", null, createElementFactory("var", {})], ["video", null, createElementFactory("video", {})], + ["wbr", null, createElementFactory("wbr", {})], // https://rawgit.com/w3c/aria/stable/#conflict_resolution_presentation_none ["presentational with accessible name", "img", createElementFactory("img", {alt: "", 'aria-label': "foo"})], ["presentational

    global aria attributes", "heading", createElementFactory("h1", {'aria-describedby': "comment-1", role: "presentation"})], ["presentational

    global aria attributes", "heading", createElementFactory("h1", {'aria-describedby': "comment-1", role: "none"})], - //
    isn't mapped to `"generic"` yet so implicit semantics are `No role` - ["presentational
    with prohibited aria attributes", null, createElementFactory("div", {'aria-label': "hello", role: "presentation"})], - ["presentational
    with prohibited aria attributes", null, createElementFactory("div", {'aria-label': "hello", role: "none"})], + // isn't mapped to `"generic"` yet so implicit semantics are `No role` + ["presentational with prohibited aria attributes", null, createElementFactory("abbr", {'aria-label': "hello", role: "presentation"})], + ["presentational with prohibited aria attributes", null, createElementFactory("abbr", {'aria-label': "hello", role: "none"})], ]; it.each(cases)("%s has the role %s", (name, role, elementFactory) => { diff --git a/sources/accessible-name-and-description.ts b/sources/accessible-name-and-description.ts index 08d04476..aacd206e 100644 --- a/sources/accessible-name-and-description.ts +++ b/sources/accessible-name-and-description.ts @@ -12,6 +12,7 @@ import { isHTMLTextAreaElement, safeWindow, isHTMLFieldSetElement, + isHTMLLabelElement, isHTMLLegendElement, isHTMLOptGroupElement, isHTMLTableElement, @@ -91,7 +92,7 @@ function isHidden( /** * @param {Node} node - - * @returns {boolean} - As defined in step 2E of https://w3c.github.io/accname/#mapping_additional_nd_te + * @returns {boolean} - As defined in step 2C of https://w3c.github.io/accname/#mapping_additional_nd_te */ function isControl(node: Node): boolean { return ( @@ -157,44 +158,44 @@ function isMarkedPresentational(node: Node): node is Element { /** * Elements specifically listed in html-aam * - * We don't need this for `label` or `legend` elements. - * Their implicit roles already allow "naming from content". - * * sources: * - * - https://w3c.github.io/html-aam/#table-element + * - https://www.w3.org/TR/html-aam-1.0/#table-element-accessible-name-computation + * - https://www.w3.org/TR/html-aam-1.0/#fieldset-element-accessible-name-computation */ function isNativeHostLanguageTextAlternativeElement( node: Node, ): node is Element { - return isHTMLTableCaptionElement(node); + return ( + isHTMLTableCaptionElement(node) || + isHTMLLegendElement(node) || + isHTMLLabelElement(node) + ); } /** - * https://w3c.github.io/aria/#namefromcontent + * https://rawgit.com/w3c/aria/stable/#namefromcontent */ function allowsNameFromContent(node: Node): boolean { return hasAnyConcreteRoles(node, [ - "button", + "button", // name required "cell", - "checkbox", - "columnheader", + "checkbox", // name required + "columnheader", // name required "gridcell", - "heading", - "label", - "legend", - "link", - "menuitem", - "menuitemcheckbox", - "menuitemradio", - "option", - "radio", + "heading", // name required + "link", // name required + "menuitem", // name required + "menuitemcheckbox", // name required + "menuitemradio", // name required + "option", // name required + "radio", // name required "row", - "rowheader", - "switch", - "tab", + "rowheader", // name required + "switch", // name required + "tab", // name required "tooltip", - "treeitem", + "treeitem", // name required ]); } @@ -429,7 +430,7 @@ export function computeTextAlternative( return null; } - // https://w3c.github.io/html-aam/#fieldset-and-legend-elements + // https://www.w3.org/TR/html-aam-1.0/#fieldset-element-accessible-name-computation if (isHTMLFieldSetElement(node)) { consultedNodes.add(node); const children = ArrayFrom(node.childNodes); @@ -444,7 +445,7 @@ export function computeTextAlternative( } } } else if (isHTMLTableElement(node)) { - // https://w3c.github.io/html-aam/#table-element + // https://www.w3.org/TR/html-aam-1.0/#table-element-accessible-name-computation consultedNodes.add(node); const children = ArrayFrom(node.childNodes); for (let i = 0; i < children.length; i += 1) { @@ -469,8 +470,8 @@ export function computeTextAlternative( } return null; } else if (getLocalName(node) === "img" || getLocalName(node) === "area") { - // https://w3c.github.io/html-aam/#area-element - // https://w3c.github.io/html-aam/#img-element + // https://www.w3.org/TR/html-aam-1.0/#area-element-accessible-name-computation + // https://www.w3.org/TR/html-aam-1.0/#img-element-accessible-name-computation const nameFromAlt = useAttribute(node, "alt"); if (nameFromAlt !== null) { return nameFromAlt; @@ -488,7 +489,7 @@ export function computeTextAlternative( node.type === "submit" || node.type === "reset") ) { - // https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-description-computation + // https://www.w3.org/TR/html-aam-1.0/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation const nameFromValue = useAttribute(node, "value"); if (nameFromValue !== null) { return nameFromValue; @@ -521,7 +522,7 @@ export function computeTextAlternative( .join(" "); } - // https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation + // https://www.w3.org/TR/html-aam-1.0/#input-type-image-accessible-name-computation // TODO: wpt test consider label elements but html-aam does not mention them // We follow existing implementations over spec if (isHTMLInputElement(node) && node.type === "image") { @@ -540,7 +541,7 @@ export function computeTextAlternative( } if (hasAnyConcreteRoles(node, ["button"])) { - // https://www.w3.org/TR/html-aam-1.0/#button-element + // https://www.w3.org/TR/html-aam-1.0/#button-element-accessible-name-computation const nameFromSubTree = computeMiscTextAlternative(node, { isEmbeddedInLabel: false, isReferenced: false, diff --git a/sources/accessible-name.ts b/sources/accessible-name.ts index 55cc43dd..93c9bef3 100644 --- a/sources/accessible-name.ts +++ b/sources/accessible-name.ts @@ -5,7 +5,7 @@ import { import { hasAnyConcreteRoles } from "./util"; /** - * https://w3c.github.io/aria/#namefromprohibited + * https://rawgit.com/w3c/aria/stable/#namefromprohibited */ function prohibitsNaming(node: Node): boolean { return hasAnyConcreteRoles(node, [ @@ -21,6 +21,7 @@ function prohibitsNaming(node: Node): boolean { "strong", "subscript", "superscript", + "time", ]); } diff --git a/sources/getRole.ts b/sources/getRole.ts index 14096a97..376a94fb 100644 --- a/sources/getRole.ts +++ b/sources/getRole.ts @@ -1,4 +1,4 @@ -// https://w3c.github.io/html-aria/#document-conformance-requirements-for-use-of-aria-attributes-in-html +// https://www.w3.org/TR/html-aria/#document-conformance-requirements-for-use-of-aria-attributes-in-html import { presentationRoles } from "./util"; @@ -15,56 +15,96 @@ export function getLocalName(element: Element): string { ); } +// https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings const localNameToRoleMappings: Record = { + address: "group", article: "article", + // WARNING: Only in certain context aside: "complementary", + b: "generic", + bdi: "generic", + bdo: "generic", + blockquote: "blockquote", + body: "generic", button: "button", + caption: "caption", + code: "code", + data: "generic", datalist: "listbox", - dd: "definition", + del: "deletion", details: "group", + dfn: "term", dialog: "dialog", - dt: "term", + div: "generic", + em: "emphasis", fieldset: "group", figure: "figure", + // WARNING: Only in certain context + footer: "contentinfo", // WARNING: Only with an accessible name form: "form", - footer: "contentinfo", h1: "heading", h2: "heading", h3: "heading", h4: "heading", h5: "heading", h6: "heading", + // WARNING: Only in certain context header: "banner", + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aria/issues/451 + hgroup: "group", hr: "separator", html: "document", - legend: "legend", + i: "generic", + ins: "insertion", + // WARNING: Only in certain context li: "listitem", - math: "math", main: "main", + math: "math", menu: "list", + meter: "meter", nav: "navigation", ol: "list", optgroup: "group", // WARNING: Only in certain context option: "option", output: "status", + p: "paragraph", + pre: "generic", progress: "progressbar", + q: "generic", + // WARNING: html-aria and html-aam conflict on this role assignment + // REF: https://github.com/w3c/html-aria/issues/466 + s: "deletion", + samp: "generic", + search: "search", // WARNING: Only with an accessible name section: "region", + small: "generic", + span: "generic", + strong: "strong", + sub: "subscript", + // WARNING: Following user agent precedent in preference of specification summary: "button", + sup: "superscript", + svg: "graphics-document", table: "table", tbody: "rowgroup", + // WARNING: Only in certain context + td: "cell", textarea: "textbox", tfoot: "rowgroup", // WARNING: Only in certain context - td: "cell", th: "columnheader", thead: "rowgroup", + time: "time", tr: "row", + u: "generic", ul: "list", }; +// https://rawgit.com/w3c/aria/stable/#role_definitions const prohibitedAttributes: Record> = { caption: new Set(["aria-label", "aria-labelledby"]), code: new Set(["aria-label", "aria-labelledby"]), @@ -77,7 +117,9 @@ const prohibitedAttributes: Record> = { presentation: new Set(["aria-label", "aria-labelledby"]), strong: new Set(["aria-label", "aria-labelledby"]), subscript: new Set(["aria-label", "aria-labelledby"]), + suggestion: new Set(["aria-label", "aria-labelledby"]), superscript: new Set(["aria-label", "aria-labelledby"]), + time: new Set(["aria-label", "aria-labelledby"]), }; /** @@ -95,15 +137,16 @@ function hasGlobalAriaAttributes(element: Element, role: string): boolean { "aria-current", "aria-description", "aria-describedby", + "aria-description", // Deviated from "stable" in anticipation of Editor's Draft "aria-details", - // "disabled", + // "aria-disabled", "aria-dropeffect", - // "errormessage", + // "aria-errormessage", "aria-flowto", "aria-grabbed", - // "haspopup", + // "aria-haspopup", "aria-hidden", - // "invalid", + // "aria-invalid", "aria-keyshortcuts", "aria-label", "aria-labelledby", @@ -151,11 +194,10 @@ function getImplicitRole(element: Element): string | null { switch (getLocalName(element)) { case "a": case "area": - case "link": if (element.hasAttribute("href")) { return "link"; } - break; + return "generic"; case "img": if ( element.getAttribute("alt") === "" && diff --git a/sources/util.ts b/sources/util.ts index f0741810..a8e17675 100644 --- a/sources/util.ts +++ b/sources/util.ts @@ -59,6 +59,12 @@ export function isHTMLFieldSetElement( return isElement(node) && getLocalName(node) === "fieldset"; } +export function isHTMLLabelElement( + node: Node | null +): node is HTMLLabelElement { + return isElement(node) && getLocalName(node) === "label"; +} + export function isHTMLLegendElement( node: Node | null, ): node is HTMLLegendElement { diff --git a/tests/cypress/integration/web-platform-test.cy.js b/tests/cypress/integration/web-platform-test.cy.js index 428af590..42fe5f6c 100644 --- a/tests/cypress/integration/web-platform-test.cy.js +++ b/tests/cypress/integration/web-platform-test.cy.js @@ -89,10 +89,10 @@ context("wpt", () => { ["name_test_case_564-manual", "pass"], ["name_test_case_565-manual", "pass"], ["name_test_case_566-manual", "pass"], - ["name_test_case_596-manual", "pass"], - ["name_test_case_597-manual", "pass"], - ["name_test_case_598-manual", "pass"], - ["name_test_case_599-manual", "pass"], + ["name_test_case_596-manual", "fail"], // https://github.com/web-platform-tests/wpt/issues/40280 + ["name_test_case_597-manual", "fail"], // https://github.com/web-platform-tests/wpt/issues/40280 + ["name_test_case_598-manual", "fail"], // https://github.com/web-platform-tests/wpt/issues/40280 + ["name_test_case_599-manual", "fail"], // https://github.com/web-platform-tests/wpt/issues/40280 ["name_test_case_600-manual", "pass"], ["name_test_case_601-manual", "pass"], ["name_test_case_602-manual", "pass"], diff --git a/tests/wpt-jsdom/to-run.yaml b/tests/wpt-jsdom/to-run.yaml index 3befe18c..f9698466 100644 --- a/tests/wpt-jsdom/to-run.yaml +++ b/tests/wpt-jsdom/to-run.yaml @@ -11,6 +11,14 @@ name_test_case_552-manual.html: [fail, getComputedStyle pseudo selector not implemented] name_test_case_553-manual.html: [fail, getComputedStyle pseudo selector not implemented] +name_test_case_596-manual.html: + [fail, https://github.com/web-platform-tests/wpt/issues/40280] +name_test_case_597-manual.html: + [fail, https://github.com/web-platform-tests/wpt/issues/40280] +name_test_case_598-manual.html: + [fail, https://github.com/web-platform-tests/wpt/issues/40280] +name_test_case_599-manual.html: + [fail, https://github.com/web-platform-tests/wpt/issues/40280] name_test_case_659-manual.html: [fail, getComputedStyle pseudo selector not implemented] name_test_case_660-manual.html: