From 834fbe6282fe2401d363b1242c90785134aa5a0b Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Mon, 27 Jan 2025 01:06:55 -0700 Subject: [PATCH 1/4] Add first-class DOM support --- .changeset/brown-pumas-design.md | 11 + .changeset/great-shrimps-turn.md | 5 + .changeset/moody-humans-rhyme.md | 5 + .changeset/tasty-emus-wonder.md | 5 + .changeset/tiny-cameras-drop.md | 15 + .github/workflows/ci.yml | 3 + README.md | 53 +- biome.json | 8 +- package.json | 3 + pnpm-lock.yaml | 849 ++++++++-- scripts/scrape-role-data.js | 2 +- src/get-elements.ts | 3 +- src/get-role.ts | 63 +- src/get-supported-attributes.ts | 44 +- src/get-supported-roles.ts | 50 +- src/is-interactive.ts | 40 +- src/lib/aria-attributes.ts | 1 + src/lib/aria-roles.ts | 1443 +++++++++++++---- src/lib/html.ts | 241 ++- src/lib/util.ts | 230 ++- src/tags/aside.ts | 31 +- src/tags/footer.ts | 13 +- src/tags/header.ts | 13 +- src/tags/input.ts | 54 +- src/tags/li.ts | 10 + src/tags/select.ts | 23 +- src/tags/td.ts | 32 +- src/tags/th.ts | 66 +- src/types.d.ts | 8 +- test/dom/get-role.test.ts | 309 ++++ test/dom/get-supported-roles.test.ts | 378 +++++ test/dom/is-interactive.test.ts | 217 +++ test/helpers.ts | 25 + test/{ => node}/data-integrity.test.ts | 19 +- test/{ => node}/get-elements.test.ts | 8 +- .../get-required-attributes.test.ts | 6 +- test/{ => node}/get-role.test.ts | 79 +- .../get-supported-attributes.test.ts | 41 +- test/{ => node}/get-supported-roles.test.ts | 32 +- test/{ => node}/html-aria.bench.ts | 22 +- test/{ => node}/is-interactive.test.ts | 16 +- test/{ => node}/is-name-required.test.ts | 4 +- test/{ => node}/utils.test.ts | 2 +- tsconfig.json | 5 +- vitest.config.ts | 4 +- vitest.workspace.ts | 36 + 46 files changed, 3590 insertions(+), 937 deletions(-) create mode 100644 .changeset/brown-pumas-design.md create mode 100644 .changeset/great-shrimps-turn.md create mode 100644 .changeset/moody-humans-rhyme.md create mode 100644 .changeset/tasty-emus-wonder.md create mode 100644 .changeset/tiny-cameras-drop.md create mode 100644 src/tags/li.ts create mode 100644 test/dom/get-role.test.ts create mode 100644 test/dom/get-supported-roles.test.ts create mode 100644 test/dom/is-interactive.test.ts rename test/{ => node}/data-integrity.test.ts (65%) rename test/{ => node}/get-elements.test.ts (96%) rename test/{ => node}/get-required-attributes.test.ts (96%) rename test/{ => node}/get-role.test.ts (89%) rename test/{ => node}/get-supported-attributes.test.ts (96%) rename test/{ => node}/get-supported-roles.test.ts (91%) rename test/{ => node}/html-aria.bench.ts (55%) rename test/{ => node}/is-interactive.test.ts (96%) rename test/{ => node}/is-name-required.test.ts (97%) rename test/{ => node}/utils.test.ts (96%) create mode 100644 vitest.workspace.ts diff --git a/.changeset/brown-pumas-design.md b/.changeset/brown-pumas-design.md new file mode 100644 index 0000000..31af292 --- /dev/null +++ b/.changeset/brown-pumas-design.md @@ -0,0 +1,11 @@ +--- +"html-aria": minor +--- + +⚠️ Breaking API changes: + + - `getRole()` now returns full role data, rather than a string. To achieve the same result, access the `name` property: + ```diff + - getRole({ tagName: 'button' }) + + getRole({ tagName: 'button' })?.name + ``` diff --git a/.changeset/great-shrimps-turn.md b/.changeset/great-shrimps-turn.md new file mode 100644 index 0000000..87deece --- /dev/null +++ b/.changeset/great-shrimps-turn.md @@ -0,0 +1,5 @@ +--- +"html-aria": patch +--- + +fix: Performance improvements for DOM API diff --git a/.changeset/moody-humans-rhyme.md b/.changeset/moody-humans-rhyme.md new file mode 100644 index 0000000..2967e08 --- /dev/null +++ b/.changeset/moody-humans-rhyme.md @@ -0,0 +1,5 @@ +--- +"html-aria": patch +--- + +feat: Add presentationalChildren property to RoleData diff --git a/.changeset/tasty-emus-wonder.md b/.changeset/tasty-emus-wonder.md new file mode 100644 index 0000000..54c2b2d --- /dev/null +++ b/.changeset/tasty-emus-wonder.md @@ -0,0 +1,5 @@ +--- +"html-aria": patch +--- + +Fix: all methods are now runnable in a DOM or DOM-like environment diff --git a/.changeset/tiny-cameras-drop.md b/.changeset/tiny-cameras-drop.md new file mode 100644 index 0000000..b12a26d --- /dev/null +++ b/.changeset/tiny-cameras-drop.md @@ -0,0 +1,15 @@ +--- +"html-aria": minor +--- + +⚠️ Breaking change: Node API now requires all attributes. + +**Attributes** + +In the previous version, `` and `` would assume `href` was set, unless you passed in an explicit `attributes: {}` object. However, in expanding the DOM API this inconsistency in behavior led to problems. Now both versions behave the same way in regards to attributes: an attribute is assumed **NOT** to exist unless passed in. + +**Ancestors** + +This behavior is largely-unchanged, however, some small improvements have been made. + +_Note: the DOM version will automatically traverse the DOM for you, and automatically reads all attributes. This change only affects the Node API where the DOM is unavailable._ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4694f77..57be294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: version: latest run_install: true - run: pnpm run build + - run: pnpm exec playwright install - run: pnpm test test-macos: runs-on: macos-latest @@ -51,6 +52,7 @@ jobs: version: latest run_install: true - run: pnpm run build + - run: pnpm exec playwright install - run: pnpm test test-windows: runs-on: windows-latest @@ -64,4 +66,5 @@ jobs: version: latest run_install: true - run: pnpm run build + - run: pnpm exec playwright install - run: pnpm test diff --git a/README.md b/README.md index 66a62c1..8b8fd5e 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,28 @@ Utilities for creating accessible HTML based on the [latest ARIA 1.3 specs](http This is designed to be a better replacement for aria-query when working with HTML. The reasons are: -- aria-query neglects the critical [HTML to ARIA spec](https://www.w3.org/TR/html-aria). With just the ARIA spec alone, it’s insufficient for working with HTML. +- aria-query neglects the critical specs [HTML Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/) and [HTML to ARIA](https://www.w3.org/TR/html-aria). With just the ARIA spec alone, it’s insufficient for working with HTML. - html-aria supports ARIA 1.3 while aria-query is still on ARIA 1.2 html-aria is also designed to be easier-to-use to prevent mistakes, smaller, is ESM tree-shakeable, and more performant (~100× faster than aria-query). -## Setup +## Usage + +### Setup ```sh npm i html-aria ``` -## Examples +### Environments + +This library works both in Node.js and the browser. But works best **when the DOM is accessible**, either the actual DOM or a virtualized one like JSDOM. The reason is the spec requires DOM traversal—identifying an element’s context in parents and children, as well as attributes of the element. In a DOM environment, html-aria will do all the work for you; in Node.js you must provide complete information about attributes, and sometimes ancestors. + +### Examples Though this library is NOT a lint plugin, it can do most of the work for you. You only need to traverse the AST of the language you’re using (e.g. HTML vs React vs Svelte), and html-aria can validate the nodes. -### ESLint + React plugin +#### Node.js (ESLint + React plugin) ```ts import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; @@ -110,7 +116,11 @@ Determine which HTML maps to which default ARIA role. ```ts import { getRole } from "html-aria"; -getRole(document.createElement("article")); // "article" +// DOM +const el = document.querySelector('article') +getRole(el); // "article" + +// Node.js (no DOM) getRole({ tagName: "input", attributes: { type: "checkbox" } }); // "checkbox" getRole({ tagName: "div", attributes: { role: "button" } }); // "button" ``` @@ -130,7 +140,11 @@ The spec dictates that **certain elements may NOT receive certain roles.** For e ```ts import { getSupportedRoles } from "html-aria"; -getSupportedRoles(document.createElement("img")); // ["none", "presentation", "img"] +// DOM +const el = document.querySelector('img') +getSupportedRoles(el); // ["none", "presentation", "img"] + +// Node.js (no DOM) getSupportedRoles({ tagName: "img", attributes: { alt: "Image caption" } }); // ["button", "checkbox", "link", (15 more)] ``` @@ -340,11 +354,17 @@ SVG is tricky. Though the [spec says](https://www.w3.org/TR/html-aria/#el-svg) ` Since we have 1 spec and 1 browser agreeing, this library defaults to `graphics-document`. Though the best answer is _SVGs should ALWAYS get an explicit `role`_. -#### Ancestor-based roles +### Node.js vs DOM behavior + +#### Node.js ignores necessary ancestor-based roles + +There are 2 categories of context-dependent element usage: **necessary** and **conditional**. + +“Necessary“ context elements require certain parents to use correctly, like table-based elements (``, ``, ``, etc.) requiring table parents (``, `
`, etc.) and list-based elements `
  • ` requiring list parents (`
      `, `
        `, ``, etc.). Without their parents, they have no purpose and their behavior is unpredictable, with some browsers even stripping elements out of the DOM. These elements will 99% of the time be used in their intended contexts. -In regards to [ARIA roles in HTML](#aria-roles-from-html), the spec gives non-semantic roles to `
  • `, ``, ``, ``, ``, ``. + +“Conditional” context elements may either have certain parents or not, all of which are valid. ``, ``, ``, ``, ``, ``. -“Conditional” context elements may either have certain parents or not, all of which are valid. `
    `, ``, and `
  • ` UNLESS they are used inside specific containers (`table`, `grid`, or `gridcell` for `
  • `/``; `list` or `menu` for `
  • `). This library assumes they’re being used in their proper containers without requiring the `ancestors` array. This is done to avoid the [footgun](https://en.wiktionary.org/wiki/footgun) of requiring missable configuration to produce accurate results, which is bad software design. +The DOM environment follows the ARIA spec. But in a Node.js context, it’s likely we are statically analyzing a component where the parents aren’t immediatly reachable—they may be in another file. If we assume the elements are used correctly even when we can’t see the ancestors, we can show more accurate errors and warnings, rather than requiring the consumer to do work that is technically and computationally difficult. -Instead, the non-semantic roles must be “opted in” by passing an explicitly-empty ancestors array: +So for the reasons above, assuming the elements are used out of context is more likely to result in less predictable behavior that could lead to mistakes. To treat elements as if they _are_ used out of their context in Node.js, pass an empty `ancestors` array as an explicit way to declare it. ```ts import { getRole } from "html-aria"; @@ -354,6 +374,21 @@ getRole({ tagName: "th" }, { ancestors: [] }); // undefined getRole({ tagName: "li" }, { ancestors: [] }); // "generic" ``` +These are all the elements that have assumed context (i.e. different behavior in Node.js): `
  • `, `
  • `, ``, `
  • `, `
    `, `
    with ancestors ['grid'] or ['treegrid'] MAY ONLY be a ['gridcell'] (all other roles are not supported) * - with NO ancestors ([]) will allow any role */ - ancestors?: AncestorList; + ancestors?: VirtualAncestorList; } /** * Given an HTML element, returns a list of supported ARIA roles for that element. * An empty array means no roles are supported (which is true for some elements!) */ -export function getSupportedRoles(element: HTMLElement | VirtualElement, options?: SupportedRoleOptions): ARIARole[] { - const { tagName, attributes } = virtualizeElement(element); +export function getSupportedRoles(element: Element | VirtualElement, options?: SupportedRoleOptions): ARIARole[] { + const tagName = getTagName(element); const tag = tags[tagName]; if (!tag) { return []; @@ -34,23 +34,27 @@ export function getSupportedRoles(element: HTMLElement | VirtualElement, options // special cases: some HTML elements require unique logic to determine supported roles based on attributes, etc. switch (tagName) { case 'a': { - return attributes && !('href' in attributes) - ? ALL_ROLES - : ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem']; // biome-ignore format: long list + const href = attr(element, 'href'); + return typeof href === 'string' ? tag.supportedRoles : ALL_ROLES; } case 'area': { - return attributes && !('href' in attributes) ? ['button', 'generic', 'link'] : tag.supportedRoles; + const href = attr(element, 'href'); + return typeof href === 'string' ? tag.supportedRoles : ['button', 'generic', 'link']; } case 'footer': case 'header': { - const role = getFooterRole(options); - return role === 'generic' ? ['generic', 'group', 'none', 'presentation'] : tag.supportedRoles; + const role = getFooterRole(element, options); + return role?.name === 'generic' ? ['generic', 'group', 'none', 'presentation'] : tag.supportedRoles; } case 'div': { - return options?.ancestors?.[0]?.tagName === 'dl' ? ['none', 'presentation'] : tag.supportedRoles; + const DL_PARENT_ROLES: ARIARole[] = ['none', 'presentation']; + if (typeof Element !== 'undefined' && element instanceof Element) { + return element.parentElement?.closest('dl:not([role])') ? DL_PARENT_ROLES : tag.supportedRoles; + } + return options?.ancestors?.[0]?.tagName === 'dl' ? DL_PARENT_ROLES : tag.supportedRoles; } case 'img': { - const name = calculateAccessibleName({ tagName, attributes }); + const name = calculateAccessibleName(element, roles.img); if (name) { /** @see https://www.w3.org/TR/html-aria/#el-img */ return ['button', 'checkbox', 'image', 'img', 'link', 'math', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option', 'progressbar', 'radio', 'scrollbar', 'separator', 'slider', 'switch', 'tab', 'treeitem']; // biome-ignore format: long list @@ -58,20 +62,23 @@ export function getSupportedRoles(element: HTMLElement | VirtualElement, options return tag.supportedRoles; } case 'li': { - return isEmptyAncestorList(options?.ancestors) ? ALL_ROLES : ['listitem']; + return hasListParent(element, options?.ancestors) ? ['listitem'] : ALL_ROLES; } case 'input': { - return getInputSupportedRoles({ attributes }); + return getInputSupportedRoles(element); } case 'select': { - return getSelectSupportedRoles({ attributes }); + return getSelectSupportedRoles(element); } case 'summary': { + if (typeof Element !== 'undefined' && element instanceof Element) { + return element.parentElement?.closest('details:not([role])') ? [] : tag.supportedRoles; + } return options?.ancestors?.some((a) => a.tagName === 'details') ? [] : tag.supportedRoles; } case 'td': { - const role = getTDRole(options); - switch (role) { + const role = getTDRole(element, options); + switch (role?.name) { case 'cell': { return ['cell']; } @@ -84,11 +91,10 @@ export function getSupportedRoles(element: HTMLElement | VirtualElement, options } } case 'th': { - // Deviation from the spec: only treat as “no corresponding role” if user has explicated this - return isEmptyAncestorList(options?.ancestors) ? ALL_ROLES : tag.supportedRoles; + return hasTableParent(element, options?.ancestors) ? tag.supportedRoles : ALL_ROLES; } case 'tr': { - return isEmptyAncestorList(options?.ancestors) ? ALL_ROLES : tag.supportedRoles; + return hasTableParent(element, options?.ancestors) ? tag.supportedRoles : ALL_ROLES; } } @@ -101,7 +107,7 @@ export function getSupportedRoles(element: HTMLElement | VirtualElement, options /** Helper function for getSupportedRoles that returns a boolean instead */ export function isSupportedRole( role: string, - element: HTMLElement | VirtualElement, + element: Element | VirtualElement, options?: SupportedRoleOptions, ): boolean { return getSupportedRoles(element, options).includes(role as ARIARole); diff --git a/src/is-interactive.ts b/src/is-interactive.ts index bf39e3f..394be7a 100644 --- a/src/is-interactive.ts +++ b/src/is-interactive.ts @@ -1,49 +1,53 @@ import { type GetRoleOptions, getRole } from './get-role.js'; -import { roles } from './lib/aria-roles.js'; -import { isDisabled, virtualizeElement } from './lib/util.js'; +import { attr, getTagName, isDisabled } from './lib/util.js'; import { getTDRole } from './tags/td.js'; import type { VirtualElement } from './types.js'; /** Given HTML, can this element be interacted with? */ -export function isInteractive(element: VirtualElement | HTMLElement, options?: GetRoleOptions): boolean { - const { tagName, attributes = {} } = virtualizeElement(element); - const role = getRole({ tagName, attributes }, options); +export function isInteractive(element: Element | VirtualElement, options?: GetRoleOptions): boolean { + const role = getRole(element, options); // separator is a special case, and does NOT care about the HTML element // @see https://www.w3.org/TR/wai-aria-1.3/#separator - if (role === 'separator') { - return 'tabindex' in attributes && 'aria-valuenow' in attributes; + if (role?.name === 'separator') { + const tabindex = attr(element, 'tabindex'); + const ariaValuenow = attr(element, 'aria-valuenow'); + return ( + (typeof tabindex === 'string' || typeof tabindex === 'number') && + (typeof ariaValuenow === 'string' || typeof ariaValuenow === 'number') + ); } // row is another special case, where it has "widget" as a superclass, // but that only applies for grid and treegrid - if (role === 'row') { - const parentTreeGrid = ['grid', 'treegrid'].includes(getTDRole(options) as string); - return parentTreeGrid && 'tabindex' in attributes; + if (role?.name === 'row') { + const parentTreeGrid = ['grid', 'treegrid'].includes(getTDRole(element, options)?.name as string); + return parentTreeGrid && !!attr(element, 'tabindex'); } if (!role) { // exception: all tags are interactive, even for nonstandard types - if (tagName === 'input' && attributes.type !== 'hidden') { + const tagName = getTagName(element); + if (tagName === 'input' && attr(element, 'type') !== 'hidden') { return true; } - return false; } // alertdialog and dialog are interactive & receive focus - if (roles[role].type.includes('window')) { + if (role.type.includes('window')) { return true; } - if (roles[role].type.includes('widget')) { - if (isDisabled(attributes)) { + if (role.type.includes('widget')) { + if (isDisabled(element)) { return false; } - const intrinsicRole = getRole({ tagName, attributes: { ...attributes, role: undefined } }, options); // ignore explicit role, but OTHER attributes may influence decision + const intrinsicRole = getRole(element, { ...options, ignoreRoleAttribute: true }); // ignore explicit role, but OTHER attributes may influence decision // if the element is not intrinsically a widget role, ALSO require tabindex - if (!intrinsicRole || !roles[intrinsicRole]?.type.includes('widget')) { - return 'tabindex' in attributes; + if (!intrinsicRole || !intrinsicRole?.type.includes('widget')) { + const tabindex = attr(element, 'tabindex'); + return typeof tabindex === 'string' || typeof tabindex === 'number'; } return true; } diff --git a/src/lib/aria-attributes.ts b/src/lib/aria-attributes.ts index d9687ef..3f4963d 100644 --- a/src/lib/aria-attributes.ts +++ b/src/lib/aria-attributes.ts @@ -8,6 +8,7 @@ import type { WidgetAttribute, } from '../types.js'; +// note: all fields required to be monomorphic export const globalAttributes: Record = { 'aria-atomic': { category: ['global', 'liveregion'], type: 'boolean', default: false }, 'aria-braillelabel': { category: ['global'], type: 'string' }, diff --git a/src/lib/aria-roles.ts b/src/lib/aria-roles.ts index f576117..48159ac 100644 --- a/src/lib/aria-roles.ts +++ b/src/lib/aria-roles.ts @@ -1,6 +1,7 @@ import type { ARIAAttribute, ARIARole, + AbstractRole, DocumentStructureRole, GraphicsRole, LandmarkRole, @@ -12,24 +13,48 @@ import type { export type RoleType = 'abstract' | 'widget' | 'document' | 'landmark' | 'liveregion' | 'window' | 'graphics'; +// note: all fields required to be monomorphic export interface RoleData { - type: RoleType[]; /** - * If given, states that this role can only exist within this container - * @see https://www.w3.org/TR/wai-aria-1.3/#scope + * A list of roles which are allowed on an accessibility child (simplified as "child") of the element with this role. + * @see https://w3c.github.io/aria/#mustContain */ - requiredParents?: ARIARole[]; - superclasses?: ARIARole[]; // ignores abstract roles - subclasses?: ARIARole[]; // ignores abstract roles + allowedChildRoles: ARIARole[]; /** - * Role that require an accessible name. + * The DOM descendants are presentational. + * @see https://w3c.github.io/aria/#childrenArePresentational */ + childrenPresentational: boolean; + /** Default values for ARIA attributes (if any) */ + defaultAttributeValues: Record; + /** Which HTML elements inherit this role, if any (note: attributes may be necessary) */ + elements: VirtualElement[]; + name: ARIARole; + /** + * @see https://w3c.github.io/aria/#namefromauthor + * @see https://w3c.github.io/aria/#namefromcontent + * @see https://w3c.github.io/aria/#namefromprohibited + */ + nameFrom: 'author' | 'authorAndContents' | 'contents' | 'prohibited'; + /** Role that require an accessible name. */ nameRequired: boolean; + /** + * aria-* attributes that are explicitly prohibited for this role, and are considered an error if set. + * @see https://www.w3.org/TR/wai-aria-1.3/#prohibitedattributes + */ + prohibited: ARIAAttribute[]; + /** + * If given, states that this role can only exist within this container + * @see https://www.w3.org/TR/wai-aria-1.3/#scope + */ + requiredParentRoles: ARIARole[]; /** * aria-* attributes that MUST be set for this role. * @see https://www.w3.org/TR/wai-aria-1.3/#requiredState */ required: ARIAAttribute[]; + superclasses: (ARIARole | AbstractRole)[]; // ignores abstract roles + subclasses: (ARIARole | AbstractRole)[]; // ignores abstract roles /** * aria-* attributes that MAY be set for this role. * Note: this includes required attributes, supported attributes, and inherited attributes from superclass role types. @@ -37,865 +62,1588 @@ export interface RoleData { * @see https://www.w3.org/TR/wai-aria-1.3/#inheritedattributes */ supported: ARIAAttribute[]; - /** - * aria-* attributes that are explicitly prohibited for this role, and are considered an error if set. - * @see https://www.w3.org/TR/wai-aria-1.3/#prohibitedattributes - */ - prohibited: ARIAAttribute[]; - /** - * Which HTML elements inherit this role, if any (note: attributes may be necessary) - */ - elements: VirtualElement[] | undefined; + type: RoleType[]; } export const widgetRoles: Record = { /** An input that allows for user-triggered actions when clicked or pressed. See related link. */ button: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'button' }], + name: 'button', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['command'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-pressed', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'button', attributes: { type: 'button' } }], + type: ['widget'], }, /** A checkable input that has three possible values: true, false, or mixed. */ checkbox: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'checkbox' } }], + name: 'checkbox', + nameFrom: 'authorAndContents', nameRequired: true, - subclasses: ['switch'], + prohibited: [], required: ['aria-checked'], + requiredParentRoles: [], + subclasses: ['switch'], + superclasses: ['input'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'checkbox' } }], + type: ['widget'], }, /** An input that controls another element, such as a listbox or grid, that can dynamically pop up to help the user set the value of the input. */ combobox: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-haspopup': 'listbox', + }, + elements: [{ tagName: 'select' }], + name: 'combobox', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: ['aria-expanded'], + requiredParentRoles: [], + subclasses: [], + superclasses: [], supported: ['aria-activedescendant', 'aria-atomic', 'aria-autocomplete', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'select' }], + type: ['widget'], }, /** A composite widget containing a collection of one or more rows with one or more cells where some or all cells in the grid are focusable by using methods of two-dimensional navigation, such as directional arrow keys. */ grid: { - type: ['widget'], - superclasses: ['table'], - subclasses: ['treegrid'], + allowedChildRoles: ['caption', 'row', 'rowgroup'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'table', attributes: { role: 'grid' } }], + name: 'grid', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['treegrid'], + superclasses: ['composite', 'table'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colcount', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-roledescription', 'aria-rowcount'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'table', attributes: { role: 'grid' } }], + type: ['widget'], }, /** A cell in a grid or treegrid. */ gridcell: { - type: ['widget'], - superclasses: ['cell'], - subclasses: ['columnheader', 'rowheader'], - nameRequired: false, - required: [], - supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-selected'], // biome-ignore format: long list - prohibited: [], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, elements: [ { tagName: 'td', attributes: { role: 'gridcell' } }, { tagName: 'th', attributes: { role: 'gridcell' } }, ], + name: 'gridcell', + nameFrom: 'authorAndContents', + nameRequired: false, + prohibited: [], + required: [], + requiredParentRoles: [], + subclasses: ['columnheader', 'rowheader'], + superclasses: ['cell', 'widget'], + supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-selected'], // biome-ignore format: long list + type: ['widget'], }, /** An interactive reference to an internal or external resource that, when activated, causes the user agent to navigate to that resource. See related button. */ link: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'a' }, { tagName: 'area' }], + name: 'link', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: [], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'a' }, { tagName: 'area' }], + type: ['widget'], }, /** A widget that allows the user to select one or more items from a list of choices. See related combobox and list. */ listbox: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'vertical', + }, + elements: [{ tagName: 'select', attributes: { multiple: true } }], + name: 'listbox', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['select'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'select', attributes: { multiple: true } }], + type: ['widget'], }, /** A type of widget that offers a list of choices to the user. */ menu: { - type: ['widget'], - subclasses: ['menubar'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'vertical', + }, + elements: [], + name: 'menu', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['menubar'], + superclasses: ['select'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A presentation of menu that usually remains visible and is usually presented horizontally. */ menubar: { - type: ['widget'], - superclasses: ['menu'], + allowedChildRoles: ['group', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'separator'], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'horizontal', + }, + elements: [], + name: 'menubar', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['menu'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** An option in a set of choices contained by a menu or menubar. */ menuitem: { - type: ['widget'], - subclasses: ['menuitemcheckbox', 'menuitemradio'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'menuitem', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: ['menu', 'menubar'], + subclasses: ['menuitemcheckbox', 'menuitemradio'], + superclasses: ['command'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A menuitem with a checkable state whose possible values are true, false, or mixed. */ menuitemcheckbox: { - type: ['widget'], - superclasses: ['menuitem'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [], + name: 'menuitemcheckbox', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: ['aria-checked'], + requiredParentRoles: ['menu', 'menubar'], + subclasses: [], + superclasses: ['menuitem'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A checkable menuitem in a set of elements with the same role, only one of which can be checked at a time. */ menuitemradio: { - type: ['widget'], - superclasses: ['menuitem'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [], + name: 'menuitemradio', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: ['aria-checked'], + requiredParentRoles: ['menu', 'menubar'], + subclasses: [], + superclasses: ['menuitem'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** An item in a listbox. */ option: { - type: ['widget'], - subclasses: ['treeitem'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'option' }], + name: 'option', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: ['listbox'], + subclasses: ['treeitem'], + superclasses: ['input'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-selected', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'option' }], + type: ['widget'], }, /** An element that displays the progress status for tasks that take a long time. */ progressbar: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-valuemin': '0', + 'aria-valuemax': '100', + }, + elements: [{ tagName: 'progress' }], + name: 'progressbar', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['range', 'widget'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'progress' }], + type: ['widget'], }, /** A checkable input in a group of elements with the same role, only one of which can be checked at a time. */ radio: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'radio' } }], + name: 'radio', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: ['aria-checked'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['input'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'radio' } }], + type: ['widget'], }, /** A group of radio buttons. */ radiogroup: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'radiogroup', + nameFrom: 'author', nameRequired: true, - subclasses: ['listbox', 'menu', 'radiogroup', 'row', 'toolbar', 'tree'], + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['list'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A row of cells in a tabular container. */ row: { - type: ['document', 'widget'], - superclasses: ['group'], + allowedChildRoles: ['cell', 'columnheader', 'gridcell', 'rowheader'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'tr' }], + name: 'row', + nameFrom: 'authorAndContents', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: ['grid', 'table', 'treegrid', 'rowgroup'], + subclasses: [], + superclasses: ['group', 'widget'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-level', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-selected', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'tr' }], + type: ['document', 'widget'], }, /** A graphical object that controls the scrolling of content within a viewing area, regardless of whether the content is fully displayed within the viewing area. */ scrollbar: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-orientation': 'vertical', + 'aria-valuemax': '100', + 'aria-valuemin': '0', + }, + elements: [], + name: 'scrollbar', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: ['aria-controls', 'aria-valuenow'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['range', 'widget'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A type of textbox intended for specifying search criteria. See related textbox and search. */ searchbox: { - type: ['widget'], - superclasses: ['textbox'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'search' } }], + name: 'searchbox', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['textbox'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-autocomplete', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiline', 'aria-owns', 'aria-placeholder', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'search' } }], + type: ['widget'], }, /** A divider that separates and distinguishes sections of content or groups of menuitems. */ separator: { - type: ['widget', 'document'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-orientation': 'horizontal', + 'aria-valuemax': '100', + 'aria-valuemin': '0', + }, + elements: [{ tagName: 'hr' }], + name: 'separator', + nameFrom: 'author', nameRequired: false, - required: [], - supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuetext'], // biome-ignore format: long list prohibited: [], - elements: [{ tagName: 'hr' }], + required: [], // aria-valuenow (if focusable) + requiredParentRoles: [], + subclasses: [], + superclasses: ['structure', 'widget'], + supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuetext'], // biome-ignore format: long list + type: ['widget', 'document'], }, /** An input where the user selects a value from within a given range. */ slider: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-orientation': 'horizontal', + 'aria-valuemax': '100', + 'aria-valuemin': '0', + }, + elements: [{ tagName: 'input', attributes: { type: 'range' } }], + name: 'slider', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: ['aria-valuenow'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['input', 'range'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'range' } }], + type: ['widget'], }, /** A form of range that expects the user to select from among discrete choices. */ spinbutton: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'number' } }], + name: 'spinbutton', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['composite', 'input', 'range'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'number' } }], + type: ['widget'], }, /** A type of checkbox that represents on/off values, as opposed to checked/unchecked values. See related checkbox. */ switch: { - type: ['widget'], - superclasses: ['checkbox'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'checkbox', role: 'switch' } }], + name: 'switch', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: ['aria-checked'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['checkbox'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'checkbox', role: 'switch' } }], + type: ['widget'], }, /** A grouping label providing a mechanism for selecting the tab content that is to be rendered to the user. */ tab: { - type: ['widget'], - requiredParents: ['tablist'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-selected': 'false', + }, + elements: [{ tagName: 'button', attributes: { type: 'button', role: 'tab' } }], + name: 'tab', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: ['tablist'], + subclasses: [], + superclasses: ['sectionhead', 'widget'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-selected', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'button', attributes: { type: 'button', role: 'tab' } }], + type: ['widget'], }, /** A list of tab elements, which are references to tabpanel elements. */ tablist: { - type: ['widget'], - nameRequired: false, - required: [], - supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'horizontal', + }, elements: [ { tagName: 'menu', attributes: { role: 'tablist' } }, { tagName: 'ol', attributes: { role: 'tablist' } }, { tagName: 'ul', attributes: { role: 'tablist' } }, ], + name: 'tablist', + nameFrom: 'author', + nameRequired: false, + prohibited: [], + required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['composite'], + supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list + type: ['widget'], }, /** A container for the resources associated with a tab, where each tab is contained in a tablist. */ tabpanel: { - type: ['widget'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'tabpanel', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['tabpanel'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, /** A type of input that allows free-form text as its value. */ textbox: { - type: ['widget'], - subclasses: ['searchbox'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'input', attributes: { type: 'text' } }], + name: 'textbox', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['searchbox'], + superclasses: ['input'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-autocomplete', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiline', 'aria-owns', 'aria-placeholder', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'input', attributes: { type: 'text' } }], + type: ['widget'], }, /** A widget that allows the user to select one or more items from a hierarchically organized collection. */ tree: { - type: ['widget'], - subclasses: ['treegrid'], - nameRequired: true, - required: [], - supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], + allowedChildRoles: ['group', 'treeitem'], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'vertical', + }, elements: [ { tagName: 'menu', attributes: { role: 'tree' } }, { tagName: 'ol', attributes: { role: 'tree' } }, { tagName: 'ul', attributes: { role: 'tree' } }, ], + name: 'tree', + nameFrom: 'author', + nameRequired: true, + prohibited: [], + required: [], + requiredParentRoles: [], + subclasses: ['treegrid'], + superclasses: ['select'], + supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-required', 'aria-roledescription'], // biome-ignore format: long list + type: ['widget'], }, /** A grid whose rows can be expanded and collapsed in the same manner as for a tree. */ treegrid: { - type: ['widget'], + allowedChildRoles: ['caption', 'row', 'rowgroup'], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'vertical', // inherited from tree + }, + elements: [{ tagName: 'table', attributes: { role: 'treegrid' } }], + name: 'treegrid', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['grid', 'tree'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colcount', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowcount'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'table', attributes: { role: 'treegrid' } }], + type: ['widget'], }, /** An item in a tree. */ treeitem: { - type: ['widget'], - superclasses: ['listitem', 'option'], - requiredParents: ['tree', 'group', 'treeitem'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'treeitem', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: ['tree', 'group'], + subclasses: [], + superclasses: ['listitem', 'option'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-level', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-selected', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['widget'], }, }; export const documentRoles: Record = { /** A structure containing one or more focusable elements requiring user input, such as keyboard or gesture events, that do not follow a standard interaction pattern supported by a widget role. */ application: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'application', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['structure'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** A section of a page that consists of a composition that forms an independent part of a document, page, or site. */ article: { - type: ['document'], - superclasses: ['document'], - subclasses: ['comment'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'article' }], + name: 'article', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['comment'], + superclasses: ['document'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'article' }], + type: ['document'], }, /** A section of content that is quoted from another source. */ blockquote: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'blockquote' }], + name: 'blockquote', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'blockquote' }], + type: ['document'], }, /** Visible content that names, or describes a figure, grid, group, radiogroup, table or treegrid. */ caption: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'caption' }], + name: 'caption', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: ['figure', 'grid', 'group', 'radiogroup', 'table', 'treegrid'], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'caption' }], + type: ['document'], }, /** A cell in a tabular container. See related gridcell. */ cell: { - type: ['document'], - subclasses: ['columnheader', 'gridcell', 'rowheader'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'td' }], + name: 'cell', + nameFrom: 'authorAndContents', nameRequired: false, - requiredParents: ['row'], + prohibited: [], required: [], + requiredParentRoles: ['row'], + subclasses: ['columnheader', 'gridcell', 'rowheader'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'td' }], + type: ['document'], }, /** A section whose content represents a fragment of computer code. */ code: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'code' }], + name: 'code', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'code' }], + type: ['document'], }, /** A cell containing header information for a column. */ columnheader: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'th', attributes: { scope: 'col' } }], + name: 'columnheader', + nameFrom: 'authorAndContents', nameRequired: true, - superclasses: ['cell', 'gridcell'], - requiredParents: ['row'], + prohibited: [], required: [], + requiredParentRoles: ['row'], + subclasses: [], + superclasses: ['cell', 'gridcell', 'sectionhead'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-selected'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'th', attributes: { scope: 'col' } }], + type: ['document'], }, /** A comment contains content expressing reaction to other content. */ comment: { - type: ['document'], - superclasses: ['article'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'comment', + nameFrom: 'authorAndContents', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['article'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-level', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** A definition of a term or concept. See related term. */ definition: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'definition', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: undefined, + type: ['document'], }, /** A deletion represents content that is marked as removed, content that is being suggested for removal, or content that is no longer relevant in the context of its accompanying content. See related insertion. */ deletion: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'del' }], + name: 'deletion', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'del' }], + type: ['document'], }, /** * A list of references to members of a group, such as a static table of contents. * @deprecated in ARIA 1.2 */ directory: { - type: ['document'], - superclasses: ['list'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'directory', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['list'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** An element containing content that assistive technology users might want to browse in a reading mode. */ document: { - type: ['document'], - subclasses: ['article'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'html' }], + name: 'document', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['article'], + superclasses: ['structure'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'html' }], + type: ['document'], }, /** One or more emphasized characters. See related strong. */ emphasis: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'em' }], + name: 'emphasis', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'em' }], + type: ['document'], }, /** A scrollable list of articles where scrolling might cause articles to be added to or removed from either end of the list. */ feed: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'feed', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['article'], + superclasses: ['list'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** A perceivable section of content that typically contains a graphical document, images, media player, code snippets, or example text. The parts of a figure MAY be user-navigable. */ figure: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'figure' }], + name: 'figure', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'figure' }], + type: ['document'], }, /** A nameless container element that has no semantic meaning on its own. */ generic: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'b' }, { tagName: 'i' }, { tagName: 'pre' }, { tagName: 'q' }, { tagName: 'samp' }, { tagName: 'small' }, { tagName: 'span' }, { tagName: 'u' }], // biome-ignore format: long list + name: 'generic', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-brailleroledescription', 'aria-label', 'aria-labelledby', 'aria-roledescription'], // biome-ignore format: long list required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['structure'], supported: ['aria-atomic', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-brailleroledescription', 'aria-label', 'aria-labelledby', 'aria-roledescription'], // biome-ignore format: long list - elements: [{ tagName: 'b' }, { tagName: 'i' }, { tagName: 'pre' }, { tagName: 'q' }, { tagName: 'samp' }, { tagName: 'small' }, { tagName: 'span' }, { tagName: 'u' }], // biome-ignore format: long list + type: ['document'], }, /** A set of user interface objects that is not intended to be included in a page summary or table of contents by assistive technologies. */ group: { - type: ['document'], - subclasses: ['row', 'toolbar'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'fieldset' }, { tagName: 'address' }, { tagName: 'details' }, { tagName: 'hgroup' }, { tagName: 'optgroup' }], // biome-ignore format: long list + name: 'group', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['row', 'select', 'toolbar'], + superclasses: ['section'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'fieldset' }, { tagName: 'address' }, { tagName: 'details' }, { tagName: 'hgroup' }, { tagName: 'optgroup' }], // biome-ignore format: long list + type: ['document'], }, /** A heading for a section of the page. */ heading: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'h1' }, { tagName: 'h2' }, { tagName: 'h3' }, { tagName: 'h4' }, { tagName: 'h5' }, { tagName: 'h6' }], // biome-ignore format: long list + name: 'heading', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: ['aria-level'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['sectionhead'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-level', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'h1' }, { tagName: 'h2' }, { tagName: 'h3' }, { tagName: 'h4' }, { tagName: 'h5' }, { tagName: 'h6' }], // biome-ignore format: long list + type: ['document'], }, /** Synonym of img. */ image: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: {}, + elements: [{ tagName: 'img' }], + name: 'image', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['graphics-symbol'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'img' }], + type: ['document'], }, /** A container for a collection of elements that form an image. See synonym image. */ img: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'img' }], + name: 'img', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['graphics-symbol'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'img' }], + type: ['document'], }, /** An insertion contains content that is marked as added or content that is being suggested for addition. See related deletion. */ insertion: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'ins' }], + name: 'insertion', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'ins' }], + type: ['document'], }, /** A section containing listitem elements. See related listbox. */ list: { - type: ['document'], + allowedChildRoles: ['listitem'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'ul' }, { tagName: 'ol' }, { tagName: 'menu' }], + name: 'list', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['directory', 'feed'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'ul' }, { tagName: 'ol' }, { tagName: 'menu' }], + type: ['document'], }, /** A single item in a list or directory. */ listitem: { - type: ['document'], - subclasses: ['treeitem'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'li' }], + name: 'listitem', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: ['directory', 'list'], + subclasses: ['treeitem'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-posinset', 'aria-relevant', 'aria-roledescription', 'aria-setsize'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'li' }], + type: ['document'], }, /** Content which is marked or highlighted for reference or notation purposes, due to the content's relevance in the enclosing context. */ mark: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'mark' }], + name: 'mark', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'mark' }], + type: ['document'], }, /** Content that represents a mathematical expression. */ math: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'math' }], + name: 'math', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'math' }], + type: ['document'], }, /** An element that represents a scalar measurement within a known range, or a fractional value. See related progressbar. */ meter: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: true, + defaultAttributeValues: { + 'aria-valuemin': '0', + 'aria-valuemax': '100', + }, + elements: [], + name: 'meter', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: ['aria-valuenow'], + requiredParentRoles: [], + subclasses: [], + superclasses: ['range'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** An element whose implicit native role semantics will not be mapped to the accessibility API. See synonym presentation. */ none: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'none', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['structure'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: undefined, + type: ['document'], }, /** A section whose content represents additional information or parenthetical context to the primary content it supplements. */ note: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'note', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, /** A paragraph of content. */ paragraph: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'p' }], + name: 'paragraph', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'p' }], + type: ['document'], }, /** Synonym of none */ presentation: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'presentation', + nameFrom: 'author', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['structure'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: undefined, + type: ['document'], }, /** A row of cells in a tabular container. */ - row: { ...widgetRoles.row }, + row: widgetRoles.row, /** A structure containing one or more row elements in a tabular container. */ rowgroup: { - type: ['document'], + allowedChildRoles: ['row'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'tbody' }, { tagName: 'tfoot' }, { tagName: 'thead' }], + name: 'rowgroup', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: ['grid', 'table', 'treegrid'], + subclasses: [], + superclasses: ['structure'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'tbody' }, { tagName: 'tfoot' }, { tagName: 'thead' }], + type: ['document'], }, /** A cell containing header information for a row. */ rowheader: { - type: ['document'], - superclasses: ['cell', 'gridcell'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'th', attributes: { scope: 'row' } }], + name: 'rowheader', + nameFrom: 'authorAndContents', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: ['row'], + subclasses: [], + superclasses: ['cell', 'gridcell', 'sectionhead'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-selected', 'aria-sort'], // biome-ignore format: long list + type: ['document'], + }, + /** A set of user interface objects and information representing information about its closest ancestral content group. */ + sectionfooter: { + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'sectionfooter', + nameFrom: 'author', + nameRequired: false, prohibited: [], - elements: [{ tagName: 'th', attributes: { scope: 'row' } }], + required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], + supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list + type: ['document'], }, - /** A divider that separates and distinguishes sections of content or groups of menuitems. */ - separator: { - ...(widgetRoles.separator as RoleData), + /** A set of user interface objects and information that represents a collection of introductory items for the element's closest ancestral content group. */ + sectionheader: { + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'sectionheader', + nameFrom: 'author', + nameRequired: false, + prohibited: [], + required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], + supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list + type: ['document'], }, + /** A divider that separates and distinguishes sections of content or groups of menuitems. */ + separator: widgetRoles.separator, /** Content that is important, serious, or urgent. See related emphasis. */ strong: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'strong' }], + name: 'strong', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'strong' }], + type: ['document'], }, /** One or more subscripted characters. See related superscript. */ subscript: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'sub' }], + name: 'subscript', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'sub' }], + type: ['document'], }, /** A single proposed change to content. */ suggestion: { - type: ['document'], + allowedChildRoles: ['insertion', 'deletion'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'suggestion', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: undefined, + type: ['document'], }, /** One or more superscripted characters. See related superscript. */ superscript: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'sup' }], + name: 'superscript', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'sup' }], + type: ['document'], }, /** A section containing data arranged in rows and columns. See related grid. */ table: { - type: ['document'], - subclasses: ['grid'], + allowedChildRoles: ['caption', 'row', 'rowgroup'], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'table' }], + name: 'table', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['grid'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-colcount', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription', 'aria-rowcount'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'table' }], + type: ['document'], }, /** A word or phrase with an optional corresponding definition. See related definition. */ term: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'dfn' }], + name: 'term', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['term'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'dfn' }], + type: ['document'], }, /** An element that represents a specific point in time. */ time: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'time' }], + name: 'time', + nameFrom: 'prohibited', nameRequired: false, + prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - elements: [{ tagName: 'time' }], + type: ['document'], }, /** A collection of commonly used function buttons or controls represented in compact visual form. */ toolbar: { - type: ['document'], - superclasses: ['group'], - nameRequired: false, - required: [], - supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-orientation': 'horizontal', + }, elements: [ { tagName: 'menu', attributes: { role: 'toolbar' } }, { tagName: 'ol', attributes: { role: 'toolbar' } }, { tagName: 'ul', attributes: { role: 'toolbar' } }, ], + name: 'toolbar', + nameFrom: 'author', + nameRequired: false, + prohibited: [], + required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['group'], + supported: ['aria-activedescendant', 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-orientation', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list + type: ['document'], }, /** A contextual popup that displays a description for an element. */ tooltip: { - type: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'tooltip', + nameFrom: 'authorAndContents', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['document'], }, }; export const landmarkRoles: Record = { /** A landmark that contains mostly site-oriented content, rather than page-specific content. */ banner: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'header' }], + name: 'banner', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'header' }], + type: ['landmark'], }, /** A landmark that is designed to be complementary to the main content that it is a sibling to, or a direct descendant of. The contents of a complementary landmark would be expected to remain meaningful if it were to be separated from the main content it is relevant to. */ complementary: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'aside' }], + name: 'complementary', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'aside' }], + type: ['landmark'], }, /** A landmark that contains information about the parent document. */ contentinfo: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'footer' }], + name: 'contentinfo', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'footer' }], + type: ['landmark'], }, /** A landmark region that contains a collection of items and objects that, as a whole, combine to create a form. See related search. */ form: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'form' }], + name: 'form', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'form' }], + type: ['landmark'], }, /** A landmark containing the main content of a document. */ main: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'main' }], + name: 'main', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'main' }], + type: ['landmark'], }, /** A landmark containing a collection of navigational elements (usually links) for navigating the document or related documents. */ navigation: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'nav' }], + name: 'navigation', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'nav' }], + type: ['landmark'], }, /** A landmark containing content that is relevant to a specific, author-specified purpose and sufficiently important that users will likely want to be able to navigate to the section easily and to have it listed in a summary of the page. Such a page summary could be generated dynamically by a user agent or assistive technology. */ region: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'section' }], + name: 'region', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'section' }], + type: ['landmark'], }, /** A landmark region that contains a collection of items and objects that, as a whole, combine to create a search facility. See related form and searchbox. */ search: { - type: ['landmark'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'search' }], + name: 'search', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['landmark'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'search' }], + type: ['landmark'], }, }; export const liveRegionRoles: Record = { /** A type of live region with important, and usually time-sensitive, information. See related alertdialog and status. */ alert: { - type: ['liveregion'], - subclasses: ['alertdialog'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-live': 'assertive', + 'aria-atomic': 'true', + }, + elements: [], + name: 'alert', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['alertdialog'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['liveregion'], }, /** A type of live region where new information is added in meaningful order and old information can disappear. See related marquee. */ log: { - type: ['liveregion'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-live': 'polite', + }, + elements: [], + name: 'log', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['liveregion'], }, /** A type of live region where non-essential information changes frequently. See related log. */ marquee: { - type: ['liveregion'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [], + name: 'marquee', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['liveregion'], }, /** A type of live region whose content is advisory information for the user but is not important enough to justify an alert, often but not necessarily presented as a status bar. */ status: { - type: ['liveregion'], - subclasses: ['timer'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-live': 'polite', + 'aria-atomic': 'true', + }, + elements: [{ tagName: 'output' }], + name: 'status', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['timer'], + superclasses: ['section'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'output' }], + type: ['liveregion'], }, /** A type of live region containing a numerical counter which indicates an amount of elapsed time from a start point, or the time remaining until an end point. */ timer: { - type: ['liveregion'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-live': 'off', + }, + elements: [], + name: 'timer', + nameFrom: 'author', nameRequired: false, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['status'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['liveregion'], }, }; export const windowRoles: Record = { /** A type of dialog that contains an alert message, where initial focus goes to an element within the dialog. See related alert and dialog. */ alertdialog: { - type: ['window'], - superclasses: ['alert', 'dialog'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: { + 'aria-live': 'assertive', // inherited from alert + 'aria-atomic': 'true', + }, + elements: [], + name: 'alertdialog', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['alert', 'dialog'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-modal', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: undefined, + type: ['window'], }, /** A dialog is a descendant window of the primary window of a web application. For HTML pages, the primary application window is the entire web document, i.e., the body element. */ dialog: { - type: ['window'], - subclasses: ['alertdialog'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'dialog' }], + name: 'dialog', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: ['alertdialog'], + superclasses: ['window'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-hidden', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-modal', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'dialog' }], + type: ['window'], }, }; @@ -903,31 +1651,52 @@ export const windowRoles: Record = { export const graphicsRoles: Record = { /** A type of document in which the visual appearance or layout of content conveys meaning. */ 'graphics-document': { - type: ['graphics'], - superclasses: ['document'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'svg', attributes: { role: 'graphics-document document' } }], + name: 'graphics-document', + nameFrom: 'author', nameRequired: true, + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['document'], supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'svg', attributes: { role: 'graphics-document document' } }], + type: ['graphics'], }, 'graphics-object': { - type: ['graphics'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'g', attributes: { role: 'graphics-object' } }], + name: 'graphics-object', + nameFrom: 'authorAndContents', nameRequired: false, - superclasses: ['group'], + prohibited: [], required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['group'], supported: ['aria-activedescendant', 'aria-atomic', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list - prohibited: [], - elements: [{ tagName: 'g', attributes: { role: 'graphics-object' } }], + type: ['graphics'], }, 'graphics-symbol': { - type: ['graphics'], - superclasses: ['img'], + allowedChildRoles: [], + childrenPresentational: false, + defaultAttributeValues: {}, + elements: [{ tagName: 'svg', attributes: { role: 'graphics-symbol img' } }], + name: 'graphics-symbol', + nameFrom: 'author', nameRequired: true, - required: [], - supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-description', 'aria-describedby', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list prohibited: [], - elements: [{ tagName: 'svg', attributes: { role: 'graphics-symbol img' } }], + required: [], + requiredParentRoles: [], + subclasses: [], + superclasses: ['img'], + supported: ['aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant', 'aria-roledescription'], // biome-ignore format: long list + type: ['graphics'], }, }; diff --git a/src/lib/html.ts b/src/lib/html.ts index 085fe69..7161c8c 100644 --- a/src/lib/html.ts +++ b/src/lib/html.ts @@ -19,18 +19,19 @@ export interface TagInfo { /** * If this conflicts with the role’s allowed attributes, this takes precedence. */ - supportedAttributesOverride?: ARIAAttribute[]; + supportedAttributesOverride: ARIAAttribute[] | undefined; /** * If this element doesn’t allow aria-label and related attributes by * default (Note: if a `role` is specified, this is ignored!) */ - namingProhibited?: boolean; + namingProhibited: boolean; } export const tags: Record = { // Main root html: { defaultRole: 'document', + namingProhibited: false, supportedRoles: ['document'], supportedAttributesOverride: [], }, @@ -38,31 +39,37 @@ export const tags: Record = { // Document metadata base: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: NO_ROLES, supportedAttributesOverride: [], }, head: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, link: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, meta: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, style: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, title: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, @@ -70,280 +77,368 @@ export const tags: Record = { // Sectioning root body: { defaultRole: 'generic', + namingProhibited: true, supportedRoles: ['generic'], // supports all global + generic aria-* attributes EXCEPT aria-hidden supportedAttributesOverride: ['aria-atomic', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed', 'aria-keyshortcuts', 'aria-live', 'aria-owns', 'aria-relevant'], // biome-ignore format: long list - namingProhibited: true, }, // Content sectioning address: { defaultRole: 'group', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, article: { defaultRole: 'article', + namingProhibited: false, supportedRoles: ['article', 'application', 'document', 'feed', 'main', 'none', 'presentation', 'region'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, aside: { defaultRole: 'complementary', + namingProhibited: false, supportedRoles: ['complementary', 'feed', 'none', 'note', 'presentation', 'region', 'search'], + supportedAttributesOverride: undefined, }, footer: { defaultRole: 'contentinfo', + namingProhibited: false, supportedRoles: ['contentinfo', 'generic', 'group', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, header: { defaultRole: 'banner', + namingProhibited: false, supportedRoles: ['banner', 'generic', 'group', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, h1: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, h2: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, h3: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, h4: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, h5: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, h6: { defaultRole: 'heading', + namingProhibited: false, supportedRoles: ['heading', 'none', 'presentation', 'tab'], + supportedAttributesOverride: undefined, }, hgroup: { defaultRole: 'group', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, main: { defaultRole: 'main', + namingProhibited: false, supportedRoles: ['main'], + supportedAttributesOverride: undefined, }, nav: { defaultRole: 'navigation', + namingProhibited: false, supportedRoles: ['menu', 'menubar', 'navigation', 'none', 'presentation', 'tablist'], + supportedAttributesOverride: undefined, }, section: { defaultRole: 'region', // note: for
    , we can’t determine the accessible name without scanning the entire document. Assume it’s "region". + namingProhibited: false, supportedRoles: ['alert', 'alertdialog', 'application', 'banner', 'complementary', 'contentinfo', 'dialog', 'document', 'feed', 'generic', 'group', 'log', 'main', 'marquee', 'navigation', 'none', 'note', 'presentation', 'region', 'search', 'status', 'tabpanel'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, search: { defaultRole: 'search', + namingProhibited: false, supportedRoles: ['form', 'group', 'none', 'presentation', 'region', 'search'], + supportedAttributesOverride: undefined, }, // Text content blockquote: { defaultRole: 'blockquote', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, dd: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: NO_ROLES, + supportedAttributesOverride: undefined, }, div: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, dl: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['group', 'list', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, dt: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['listitem'], + supportedAttributesOverride: undefined, }, figcaption: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ['group', 'none', 'presentation'], namingProhibited: true, + supportedRoles: ['group', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, figure: { defaultRole: 'figure', + namingProhibited: false, supportedRoles: ALL_ROLES, // Note: there are some minor behavioral quirks here which we gloss over + supportedAttributesOverride: undefined, }, hr: { defaultRole: 'separator', + namingProhibited: false, supportedRoles: ['none', 'presentation', 'separator'], + supportedAttributesOverride: undefined, }, li: { defaultRole: 'listitem', + namingProhibited: false, supportedRoles: ['listitem'], + supportedAttributesOverride: undefined, }, menu: { defaultRole: 'list', + namingProhibited: false, supportedRoles: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, ol: { defaultRole: 'list', + namingProhibited: false, supportedRoles: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, p: { defaultRole: 'paragraph', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, pre: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, ul: { defaultRole: 'list', + namingProhibited: false, supportedRoles: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, // Inline text semantics a: { defaultRole: 'link', + namingProhibited: false, supportedRoles: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, abbr: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, b: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, bdi: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, bdo: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, br: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['none', 'presentation'], supportedAttributesOverride: ['aria-hidden'], }, cite: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, code: { defaultRole: 'code', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, data: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, dfn: { defaultRole: 'term', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, em: { defaultRole: 'emphasis', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, i: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, kbd: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, mark: { defaultRole: 'mark', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, q: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, rp: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, rt: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, ruby: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, s: { defaultRole: 'deletion', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, samp: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, small: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, span: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, strong: { defaultRole: 'strong', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, sub: { defaultRole: 'subscript', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, sup: { defaultRole: 'superscript', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, time: { defaultRole: 'time', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, u: { defaultRole: 'generic', - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, var: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: ALL_ROLES, namingProhibited: true, + supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, wbr: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['none', 'presentation'], supportedAttributesOverride: ['aria-hidden'], }, @@ -351,51 +446,69 @@ export const tags: Record = { // Image and multimedia area: { defaultRole: 'link', + namingProhibited: false, supportedRoles: ['link'], + supportedAttributesOverride: undefined, }, audio: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['application'], + supportedAttributesOverride: undefined, }, img: { defaultRole: 'none', + namingProhibited: false, supportedRoles: ['img', 'image', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, map: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, track: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, video: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['application'], + supportedAttributesOverride: undefined, }, // Embedded content embed: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['application', 'document', 'img', 'image', 'none', 'presentation'], + supportedAttributesOverride: undefined, }, iframe: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['application', 'document', 'img', 'image', 'none', 'presentation'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, object: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['application', 'document', 'img', 'image'], + supportedAttributesOverride: undefined, }, picture: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: ['aria-hidden'], }, source: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, @@ -403,25 +516,33 @@ export const tags: Record = { // SVG and MathML svg: { defaultRole: 'graphics-document', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, math: { defaultRole: 'math', + namingProhibited: false, supportedRoles: ['math'], + supportedAttributesOverride: undefined, }, // Scripting canvas: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, noscript: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, script: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, @@ -430,11 +551,13 @@ export const tags: Record = { defaultRole: 'deletion', supportedRoles: ALL_ROLES, namingProhibited: true, + supportedAttributesOverride: undefined, }, ins: { defaultRole: 'insertion', supportedRoles: ALL_ROLES, namingProhibited: true, + supportedAttributesOverride: undefined, }, // Table content @@ -442,129 +565,179 @@ export const tags: Record = { defaultRole: 'caption', supportedRoles: ['caption'], namingProhibited: true, + supportedAttributesOverride: undefined, }, col: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: NO_ROLES, supportedAttributesOverride: [], }, colgroup: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: NO_ROLES, supportedAttributesOverride: [], }, table: { defaultRole: 'table', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, tbody: { defaultRole: 'rowgroup', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, td: { defaultRole: 'cell', + namingProhibited: false, supportedRoles: ['cell'], + supportedAttributesOverride: undefined, }, tfoot: { defaultRole: 'rowgroup', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, th: { defaultRole: 'columnheader', + namingProhibited: false, supportedRoles: ['cell', 'columnheader', 'gridcell', 'rowheader'], + supportedAttributesOverride: undefined, }, thead: { defaultRole: 'rowgroup', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, tr: { defaultRole: 'row', + namingProhibited: false, supportedRoles: ['row'], + supportedAttributesOverride: undefined, }, // Forms button: { defaultRole: 'button', + namingProhibited: false, supportedRoles: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + supportedAttributesOverride: undefined, }, datalist: { defaultRole: 'listbox', + namingProhibited: false, supportedRoles: ['listbox'], supportedAttributesOverride: [], }, fieldset: { defaultRole: 'group', + namingProhibited: false, supportedRoles: ['group', 'none', 'presentation', 'radiogroup'], + supportedAttributesOverride: undefined, }, form: { defaultRole: 'form', + namingProhibited: false, supportedRoles: ['form', 'none', 'presentation', 'search'], + supportedAttributesOverride: undefined, }, input: { defaultRole: 'textbox', + namingProhibited: false, supportedRoles: ['combobox', 'searchbox', 'spinbutton', 'textbox'], + supportedAttributesOverride: undefined, }, label: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: [], namingProhibited: true, + supportedRoles: [], + supportedAttributesOverride: undefined, }, legend: { defaultRole: NO_CORRESPONDING_ROLE, - supportedRoles: [], namingProhibited: true, + supportedRoles: [], + supportedAttributesOverride: undefined, }, meter: { defaultRole: 'meter', + namingProhibited: false, supportedRoles: ['meter'], + supportedAttributesOverride: undefined, }, optgroup: { defaultRole: 'group', + namingProhibited: false, supportedRoles: ['group'], + supportedAttributesOverride: undefined, }, option: { defaultRole: 'option', + namingProhibited: false, supportedRoles: ['option'], + supportedAttributesOverride: undefined, }, output: { defaultRole: 'status', + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, progress: { defaultRole: 'progressbar', + namingProhibited: false, supportedRoles: ['progressbar'], + supportedAttributesOverride: undefined, }, select: { defaultRole: 'combobox', + namingProhibited: false, supportedRoles: ['combobox', 'menu'], + supportedAttributesOverride: undefined, }, textarea: { defaultRole: 'textbox', + namingProhibited: false, supportedRoles: ['textbox'], + supportedAttributesOverride: undefined, }, // Interactive elements details: { defaultRole: 'group', + namingProhibited: false, supportedRoles: ['group'], + supportedAttributesOverride: undefined, }, dialog: { defaultRole: 'dialog', + namingProhibited: false, supportedRoles: ['alertdialog', 'dialog'], + supportedAttributesOverride: undefined, }, summary: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ALL_ROLES, + supportedAttributesOverride: undefined, }, // Web Components slot: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, template: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: [], supportedAttributesOverride: [], }, @@ -572,6 +745,8 @@ export const tags: Record = { // SVG tags (partial) g: { defaultRole: NO_CORRESPONDING_ROLE, + namingProhibited: false, supportedRoles: ['group', 'graphics-object'], + supportedAttributesOverride: undefined, }, }; diff --git a/src/lib/util.ts b/src/lib/util.ts index e52c229..5bf84ce 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,4 +1,13 @@ -import type { ARIAAttribute, AncestorList, NameProhibitedAttributes, TagName, VirtualElement } from '../types.js'; +import type { + ARIAAttribute, + ARIARole, + LandmarkRole, + NameProhibitedAttributes, + TagName, + VirtualAncestorList, + VirtualElement, +} from '../types.js'; +import { type RoleData, landmarkRoles } from './aria-roles.js'; /** Parse a list of roles, e.g. role="graphics-symbol img" */ export function parseTokenList(tokenList: string): string[] { @@ -18,84 +27,59 @@ export function firstMatchingToken(tokenList: string, validValues: T[]): T | return parseTokenList(tokenList).find((value) => validValues.includes(value as T)) as T | undefined; } -/** Are we able to traverse the DOM? */ -export function isHTMLElement(element: HTMLElement | VirtualElement): boolean { - return typeof HTMLElement !== 'undefined' && element instanceof HTMLElement; -} - -/** Normalize HTML Elements */ -export function virtualizeElement(element: HTMLElement | VirtualElement): VirtualElement { - // handle HTMLElement or VirtualElement - let tagName = '' as TagName; - let attributes: VirtualElement['attributes']; - - if (isHTMLElement(element)) { - tagName = element.tagName.toLowerCase() as TagName; - attributes = {}; - for (let i = 0; i < (element as HTMLElement).attributes.length; i++) { - // biome-ignore lint/style/noNonNullAssertion: This is guaranteed - const { name } = (element as HTMLElement).attributes[i]!; - attributes[name] = (element as HTMLElement).getAttribute(name); - } - return { tagName, attributes }; - } - - if (!element || typeof element !== 'object' || Array.isArray(element) || typeof element.tagName !== 'string') { - throw new Error(`Expected { tagName, [attributes] } object, received ${JSON.stringify(element)}`); +export function getTagName(element: Element | VirtualElement): TagName { + if (typeof Element !== 'undefined' && element instanceof Element) { + return element.tagName.toLowerCase() as TagName; } + return element.tagName as TagName; +} - return { ...element } as VirtualElement; +/** + * Get attribute from any type of element. Optimized for performance. + */ +export function attr(element: Element | VirtualElement, attribute: string) { + // Note: this is type-safe; don’t add more runtime checks to satify TypeScript + return typeof (element as Element).getAttribute === 'function' + ? (element as Element).getAttribute(attribute) + : (element as VirtualElement).attributes?.[attribute]; } /** * Determine accessible names for SOME tags (not all) * @see https://www.w3.org/TR/wai-aria-1.3/#namecalculation */ -export function calculateAccessibleName(element: VirtualElement): string | undefined { - const { tagName, attributes } = element; +export function calculateAccessibleName(element: Element | VirtualElement, role: RoleData): string | undefined { + if (role.nameFrom === 'prohibited') { + return; + } + if (role.nameFrom === 'contents') { + return 'innerText' in (element as HTMLElement) ? (element as HTMLElement).innerText : undefined; + } + // for author + authorAndContents, handle special cases first + const tagName = getTagName(element); switch (tagName) { - case 'aside': { - /** - * @see https://www.w3.org/TR/html-aam-1.0/#section-and-grouping-element-accessible-name-computation - */ - return (attributes?.['aria-label'] || attributes?.['aria-labelledby']) as string; - } case 'img': { + const alt = attr(element, 'alt'); /** * According to spec, aria-label is technically allowed for (even if alt is preferred) * @see https://www.w3.org/TR/html-aam-1.0/#img-element-accessible-name-computation */ - return (attributes?.alt || attributes?.['aria-label'] || attributes?.['aria-labelledby']) as string; - } - case 'section': { - /** - * @see https://www.w3.org/TR/html-aam-1.0/#section-and-grouping-element-accessible-name-computation - */ - return (attributes?.['aria-label'] || attributes?.['aria-labelledby']) as string; + if (alt) { + return alt as string; + } + break; } } -} -/** Is an ancestor list provided and is it empty? */ -export function isEmptyAncestorList(ancestors?: AncestorList): ancestors is [] { - return Array.isArray(ancestors) && ancestors.length === 0; -} - -/** Given ancestors, find the first matching ancestor. */ -export function firstMatchingAncestor( - validAncestors: VirtualElement[], - ancestors?: AncestorList, -): VirtualElement | undefined { - const match = (ancestors ?? []).find((a) => { - const { tagName, attributes } = virtualizeElement(a); - return validAncestors.some( - (v) => (v.attributes?.role && v.attributes.role === attributes?.role) || v.tagName === tagName, - ); - }); - if (match) { - return virtualizeElement(match); - } + return ( + (attr(element, 'aria-label') as string | undefined) || + (attr(element, 'aria-labelledby') as string | undefined) || + (role.nameFrom === 'authorAndContents' && + 'innerText' in (element as HTMLElement) && + (element as HTMLElement).innerText) || + undefined + ); } export const NAME_PROHIBITED_ATTRIBUTES = new Set([ @@ -106,31 +90,45 @@ export const NAME_PROHIBITED_ATTRIBUTES = new Set([ 'aria-roledescription', ] satisfies NameProhibitedAttributes[]); +const LANDMARK_ROLES = Object.keys(landmarkRoles) as LandmarkRole[]; +const LANDMARK_ELEMENTS: TagName[] = ['article', 'aside', 'main', 'nav', 'section']; +const LANDMARK_CSS_SELECTOR = LANDMARK_ROLES.map((role) => `[role=${role}]`) + .concat(...LANDMARK_ELEMENTS.map((el) => `${el}:not([role])`)) + .join(','); + /** Logic shared by
    and
    when determining role */ -export function hasLandmarkParent(ancestors: AncestorList) { - return !!firstMatchingAncestor( - [ - { tagName: 'article', attributes: { role: 'article' } }, - { tagName: 'aside', attributes: { role: 'complementary' } }, - { tagName: 'main', attributes: { role: 'main' } }, - { tagName: 'nav', attributes: { role: 'navigation' } }, - { tagName: 'section', attributes: { role: 'region' } }, - ], - ancestors, +export function hasLandmarkParent(element: Element | VirtualElement, ancestors?: VirtualAncestorList): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest(LANDMARK_CSS_SELECTOR); + } + return !!ancestors?.some( + (el) => LANDMARK_ELEMENTS.includes(el.tagName) || LANDMARK_ROLES.includes(attr(el, 'role') as LandmarkRole), ); } -/** Logic shared by
    and when determining role */ -export function hasGridParent(ancestors: AncestorList) { - const gridParent = firstMatchingAncestor( - [ - { tagName: 'table', attributes: { role: 'grid' } }, - { tagName: 'table', attributes: { role: 'treegrid' } }, - ], - ancestors, - ); +const LIST_TYPE_ELEMENTS: TagName[] = ['ul', 'ol', 'menu']; +const LIST_TYPE_CSS_SELECTOR = LIST_TYPE_ELEMENTS.join(','); - return gridParent?.attributes?.role === 'grid' || gridParent?.attributes?.role === 'treegrid'; +export function hasListParent(element: Element | VirtualElement, ancestors?: VirtualAncestorList): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest(LIST_TYPE_CSS_SELECTOR); + } + // special behavior: outside the DOM, if we’re testing a list-like element, assume it’s within a list + if (ancestors?.length !== 0 && element.tagName === 'li') { + return true; + } + return !!ancestors?.some((ancestor) => LIST_TYPE_ELEMENTS.includes(ancestor.tagName)); +} + +const GRID_ROLES: ARIARole[] = ['grid', 'treegrid']; +const GRID_CSS_SELECTOR = GRID_ROLES.map((role) => `[role=${role}]`).join(','); + +/** Logic shared by and when determining role */ +export function hasGridParent(element: Element | VirtualElement, ancestors?: VirtualAncestorList): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest(GRID_CSS_SELECTOR); + } + return !!ancestors?.some((el) => GRID_ROLES.includes(attr(el, 'role')! as (typeof GRID_ROLES)[number])); } export interface RemoveProhibitedOptions

    { @@ -138,6 +136,59 @@ export interface RemoveProhibitedOptions

    { prohibited?: P; } +const ROWGROUP_ROLES: ARIARole[] = ['rowgroup']; +const ROWGROUP_ELEMENTS: TagName[] = ['tfoot', 'thead']; +const ROWGROUP_CSS_SELECTOR = ROWGROUP_ROLES.map((role) => `[role=${role}]`) + .concat(...ROWGROUP_ELEMENTS.map((el) => `${el}:not([role])`)) + .join(','); + +export function hasRowgroupParent(element: Element | VirtualElement, ancestors?: VirtualAncestorList): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest(ROWGROUP_CSS_SELECTOR); + } + return !!ancestors?.some( + (el) => ROWGROUP_ELEMENTS.includes(el.tagName) || (ROWGROUP_ROLES as string[]).includes(attr(el, 'role') as string), + ); +} + +export function hasTableParent(element: Element | VirtualElement, ancestors?: VirtualAncestorList): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest('table,[role=table]'); + } + // special behavior: outside the DOM, if we’re testing a table-like element, assume it’s within a table + if ( + ancestors?.length !== 0 && + ['tbody', 'thead', 'tfoot', 'tr', 'td', 'th', 'col', 'colgroup', 'rowgroup'].includes(element.tagName) + ) { + return true; + } + return !!ancestors?.some((el) => el.tagName === 'table' || attr(el, 'role') === 'table'); +} + +const SECTIONING_CONTENT_ROLES: ARIARole[] = ['article', 'complementary', 'navigation', 'region']; +const SECTIONING_CONTENT_ELEMENTS: TagName[] = ['article', 'aside', 'nav', 'section']; +const SECTIONING_CONTENT_CSS_SELECTOR = SECTIONING_CONTENT_ROLES.map((role) => `[role=${role}]`) + .concat(...SECTIONING_CONTENT_ELEMENTS.map((el) => `${el}:not([role])`)) + .join(','); + +/** + * Has sectioning content parent + * @see https://html.spec.whatwg.org/multipage/dom.html#sectioning-content + */ +export function hasSectioningContentParent( + element: Element | VirtualElement, + ancestors?: VirtualAncestorList, +): boolean { + if (typeof Element !== 'undefined' && element instanceof Element) { + return !!element.parentElement?.closest(SECTIONING_CONTENT_CSS_SELECTOR); + } + return !!ancestors?.some( + (el) => + SECTIONING_CONTENT_ELEMENTS.includes(el.tagName) || + (SECTIONING_CONTENT_ROLES as string[]).includes(attr(el, 'role') as string), + ); +} + /** Remove prohibited aria-* attributes from a list */ export function removeProhibited( attributeList: T, @@ -153,16 +204,19 @@ export function removeProhibited; } -/** Inject attributes into an array */ -export function injectAttrs(list: ARIAAttribute[], attrs: ARIAAttribute[]): ARIAAttribute[] { - const newList = [...new Set([...list, ...attrs])]; +/** Inject new items into an array */ +export function concatDedupeAndSort(list: T[], newItems: T[]): T[] { + const newList = [...new Set([...list, ...newItems])]; newList.sort((a, b) => a.localeCompare(b)); return newList; } /** Is this element disabled? */ -export function isDisabled(attributes: Record): boolean { - return ( - String(attributes.disabled).toLowerCase() === 'true' || String(attributes['aria-disabled']).toLowerCase() === 'true' - ); +export function isDisabled(element: Element | VirtualElement): boolean { + const disabled = String(attr(element, 'disabled')); + if (disabled === '' || disabled === 'true') { + return true; + } + const ariaDisabled = String(attr(element, 'aria-disabled')); + return ariaDisabled === '' || ariaDisabled === 'true'; } diff --git a/src/tags/aside.ts b/src/tags/aside.ts index 5d72c38..6da73de 100644 --- a/src/tags/aside.ts +++ b/src/tags/aside.ts @@ -1,22 +1,15 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; import { tags } from '../lib/html.js'; -import { firstMatchingAncestor } from '../lib/util.js'; -import type { AncestorList } from '../types.js'; +import { hasSectioningContentParent } from '../lib/util.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; -function hasSectioningContentParent(ancestors: AncestorList) { - return !!firstMatchingAncestor( - [ - { tagName: 'article', attributes: { role: 'article' } }, - { tagName: 'aside', attributes: { role: 'complementary' } }, - { tagName: 'nav', attributes: { role: 'navigation' } }, - { tagName: 'section', attributes: { role: 'region' } }, - ], - ancestors, - ); -} - -export function getAsideRole({ ancestors }: { ancestors?: AncestorList } = {}) { - if (!ancestors) { - return tags.aside.defaultRole; - } - return hasSectioningContentParent(ancestors) ? 'generic' : tags.aside.defaultRole; +/** + * @see https://www.w3.org/TR/html-aam-1.0/#el-aside-ancestorbodymain + * @see https://www.w3.org/TR/html-aam-1.0/#el-aside + */ +export function getAsideRole( + element: Element | VirtualElement, + options?: { ancestors?: VirtualAncestorList }, +): RoleData | undefined { + return hasSectioningContentParent(element, options?.ancestors) ? roles.generic : roles[tags.aside.defaultRole!]; } diff --git a/src/tags/footer.ts b/src/tags/footer.ts index 04f5799..c264573 100644 --- a/src/tags/footer.ts +++ b/src/tags/footer.ts @@ -1,10 +1,11 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; import { tags } from '../lib/html.js'; import { hasLandmarkParent } from '../lib/util.js'; -import type { AncestorList } from '../types.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; -export function getFooterRole({ ancestors }: { ancestors?: AncestorList } = {}) { - if (!ancestors) { - return tags.footer.defaultRole; - } - return hasLandmarkParent(ancestors) ? 'generic' : tags.footer.defaultRole; +export function getFooterRole( + element: Element | VirtualElement, + options?: { ancestors?: VirtualAncestorList }, +): RoleData | undefined { + return hasLandmarkParent(element, options?.ancestors) ? roles.generic : roles[tags.footer.defaultRole!]; } diff --git a/src/tags/header.ts b/src/tags/header.ts index b2b6b4b..bc1576a 100644 --- a/src/tags/header.ts +++ b/src/tags/header.ts @@ -1,10 +1,11 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; import { tags } from '../lib/html.js'; import { hasLandmarkParent } from '../lib/util.js'; -import type { AncestorList } from '../types.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; -export function getHeaderRole({ ancestors }: { ancestors?: AncestorList } = {}) { - if (!ancestors) { - return tags.header.defaultRole; - } - return hasLandmarkParent(ancestors) ? 'generic' : tags.header.defaultRole; +export function getHeaderRole( + element: Element | VirtualElement, + options?: { ancestors?: VirtualAncestorList }, +): RoleData | undefined { + return hasLandmarkParent(element, options?.ancestors) ? roles.generic : roles[tags.header.defaultRole!]; } diff --git a/src/tags/input.ts b/src/tags/input.ts index 4e649f8..c80f69c 100644 --- a/src/tags/input.ts +++ b/src/tags/input.ts @@ -1,4 +1,6 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; import { NO_CORRESPONDING_ROLE, tags } from '../lib/html.js'; +import { attr } from '../lib/util.js'; import type { ARIARole, VirtualElement } from '../types.js'; export type InputType = @@ -25,49 +27,45 @@ export type InputType = | 'url' | 'week'; -export const INPUT_ROLE_MAP: Record = { - button: 'button', - checkbox: 'checkbox', +export const INPUT_ROLE_MAP: Record = { + button: roles.button, + checkbox: roles.checkbox, color: NO_CORRESPONDING_ROLE, date: NO_CORRESPONDING_ROLE, 'datetime-local': NO_CORRESPONDING_ROLE, - email: 'textbox', + email: roles.textbox, file: NO_CORRESPONDING_ROLE, hidden: NO_CORRESPONDING_ROLE, - image: 'button', + image: roles.button, month: NO_CORRESPONDING_ROLE, - number: 'spinbutton', + number: roles.spinbutton, password: NO_CORRESPONDING_ROLE, - radio: 'radio', - range: 'slider', - reset: 'button', - search: 'searchbox', - submit: 'button', - tel: 'textbox', - text: tags.input.defaultRole, + radio: roles.radio, + range: roles.slider, + reset: roles.button, + search: roles.searchbox, + submit: roles.button, + tel: roles.textbox, + text: roles.textbox, time: NO_CORRESPONDING_ROLE, - url: 'textbox', + url: roles.textbox, week: NO_CORRESPONDING_ROLE, }; const COMBOBOX_ENABLED_TYPES: InputType[] = ['email', 'url', 'search', 'tel', 'text']; -export interface GetInputRoleOptions { - attributes?: VirtualElement['attributes']; -} - -export function getInputRole({ attributes }: GetInputRoleOptions = {}) { +export function getInputRole(element: Element | VirtualElement): RoleData | undefined { // For ARIA purposes, missing or invalid types are treated as "text" - let type = attributes?.type as InputType; + let type = attr(element, 'type') as InputType | undefined; if (!type || !(type in INPUT_ROLE_MAP)) { type = 'text'; } // handle input comboboxes // @see https://www.w3.org/TR/html-aria/#el-input-text-list - const hasList = !!attributes?.list; - if (hasList && COMBOBOX_ENABLED_TYPES.includes(type)) { - return 'combobox'; + const list = attr(element, 'list'); + if (list && COMBOBOX_ENABLED_TYPES.includes(type)) { + return roles.combobox; } return (type as InputType) in INPUT_ROLE_MAP ? INPUT_ROLE_MAP[type as InputType] : INPUT_ROLE_MAP.text; @@ -98,22 +96,22 @@ export const INPUT_SUPPORTED_ROLES_MAP: Record = { week: [], }; -export function getInputSupportedRoles({ attributes }: GetInputRoleOptions = {}): ARIARole[] { +export function getInputSupportedRoles(element: Element | VirtualElement): ARIARole[] { // For ARIA purposes, missing or invalid types are treated as "text" - let type = attributes?.type as InputType; + let type = attr(element, 'type') as InputType; if (!type || !(type in INPUT_SUPPORTED_ROLES_MAP)) { type = 'text'; } // handle input comboboxes - const hasList = !!attributes?.list; - if (hasList && COMBOBOX_ENABLED_TYPES.includes(type)) { + const list = attr(element, 'list'); + if (list && COMBOBOX_ENABLED_TYPES.includes(type)) { return ['combobox']; } // special behavior: checkboxes // @see https://www.w3.org/TR/html-aria/#el-input-checkbox - if (type === 'checkbox' && 'aria-pressed' in (attributes ?? {})) { + if (type === 'checkbox' && attr(element, 'aria-pressed')) { return ['button', ...INPUT_SUPPORTED_ROLES_MAP.checkbox]; } diff --git a/src/tags/li.ts b/src/tags/li.ts new file mode 100644 index 0000000..2add304 --- /dev/null +++ b/src/tags/li.ts @@ -0,0 +1,10 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; +import { hasListParent } from '../lib/util.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; + +export function getLIRole( + element: Element | VirtualElement, + options?: { ancestors?: VirtualAncestorList }, +): RoleData | undefined { + return hasListParent(element, options?.ancestors) ? roles.listitem : roles.generic; +} diff --git a/src/tags/select.ts b/src/tags/select.ts index c3dd270..ada16ad 100644 --- a/src/tags/select.ts +++ b/src/tags/select.ts @@ -1,20 +1,21 @@ +import { type RoleData, roles } from '../lib/aria-roles.js'; import { tags } from '../lib/html.js'; +import { attr } from '../lib/util.js'; import type { ARIARole, VirtualElement } from '../types.js'; -export function getSelectRole({ attributes = {} }: { attributes?: VirtualElement['attributes'] } = {}): ARIARole { - const size = normalizeSize(attributes.size); - if ((size && size > 1) || 'multiple' in attributes) { - return 'listbox'; +export function getSelectRole(element: Element | VirtualElement): RoleData | undefined { + const size = normalizeSize(attr(element, 'size')); + const multiple = attr(element, 'multiple'); + if ((size && size > 1) || typeof multiple === 'string' || typeof multiple === 'boolean') { + return roles.listbox; } - // biome-ignore lint/style/noNonNullAssertion: this is defined - return tags.select.defaultRole!; + return roles[tags.select.defaultRole!]; } -export function getSelectSupportedRoles({ - attributes = {}, -}: { attributes?: VirtualElement['attributes'] } = {}): ARIARole[] { - const size = normalizeSize(attributes?.size); - if ((size && size > 1) || 'multiple' in attributes) { +export function getSelectSupportedRoles(element: Element | VirtualElement): ARIARole[] { + const size = normalizeSize(attr(element, 'size')); + const multiple = attr(element, 'multiple'); + if ((size && size > 1) || typeof multiple === 'string' || typeof multiple === 'boolean') { return ['listbox']; } return tags.select.supportedRoles; diff --git a/src/tags/td.ts b/src/tags/td.ts index ae098ed..cbbde49 100644 --- a/src/tags/td.ts +++ b/src/tags/td.ts @@ -1,24 +1,18 @@ -import { NO_CORRESPONDING_ROLE, tags } from '../lib/html.js'; -import { hasGridParent, isEmptyAncestorList } from '../lib/util.js'; -import type { AncestorList } from '../types.js'; +import { type RoleData, roles } from '../lib/aria-roles.js'; +import { NO_CORRESPONDING_ROLE } from '../lib/html.js'; +import { hasGridParent, hasTableParent } from '../lib/util.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; /** Special behavior for

    element */ -export function getTDRole({ ancestors }: { ancestors?: AncestorList } = {}) { - if (!ancestors) { - return tags.td.defaultRole; +export function getTDRole( + element: Element | VirtualElement, + options?: { ancestors?: VirtualAncestorList }, +): RoleData | undefined { + if (hasGridParent(element, options?.ancestors)) { + return roles.gridcell; } - - // Special behavior: require an explicitly empty ancestor array to return - // “no corresponding role” like the spec describes (if we did this by - // default, it would likely cause bad results because most users would - // likely skip this optional setup). - if (isEmptyAncestorList(ancestors)) { - return NO_CORRESPONDING_ROLE; - } - - if (hasGridParent(ancestors)) { - return 'gridcell'; + if (hasTableParent(element, options?.ancestors)) { + return roles.cell; } - - return tags.td.defaultRole; + return NO_CORRESPONDING_ROLE; } diff --git a/src/tags/th.ts b/src/tags/th.ts index d544944..d2ace0d 100644 --- a/src/tags/th.ts +++ b/src/tags/th.ts @@ -1,60 +1,42 @@ -import { NO_CORRESPONDING_ROLE, tags } from '../lib/html.js'; -import { firstMatchingAncestor, hasGridParent, isEmptyAncestorList } from '../lib/util.js'; -import type { AncestorList, VirtualElement } from '../types.js'; +import { type RoleData, roles } from '../lib/aria-roles.js'; +import { NO_CORRESPONDING_ROLE } from '../lib/html.js'; +import { attr, hasGridParent, hasRowgroupParent, hasTableParent } from '../lib/util.js'; +import type { VirtualAncestorList, VirtualElement } from '../types.js'; /** Special behavior for element */ -export function getTHRole({ - attributes, - ancestors, -}: { attributes?: VirtualElement['attributes']; ancestors?: AncestorList } = {}) { - // Special behavior: require an explicitly empty ancestor array to return - // “no corresponding role” like the spec describes (if we did this by - // default, it would likely cause bad results because most users would - // likely skip this optional setup). - if (isEmptyAncestorList(ancestors)) { - return NO_CORRESPONDING_ROLE; - } - +export function getTHRole( + element: Element | VirtualElement, + options?: { + ancestors?: VirtualAncestorList; + }, +): RoleData | undefined { // Currently deviates from specification as doesn't handle the `auto` // behaviour as that would require access to the DOM context. - switch (attributes?.scope) { - /** - * @see https://www.w3.org/TR/html-aam-1.0/#el-th-columnheader - */ + const scope = attr(element, 'scope'); + switch (scope) { + /** @see https://www.w3.org/TR/html-aam-1.0/#el-th-columnheader */ case 'col': case 'colgroup': { - return 'columnheader'; + return roles.columnheader; } - /** - * @see https://www.w3.org/TR/html-aam-1.0/#el-th-rowheader - */ + /** @see https://www.w3.org/TR/html-aam-1.0/#el-th-rowheader */ case 'row': case 'rowgroup': { - return 'rowheader'; + return roles.rowheader; } } - // See previous comment r.e. special behaviour. - if (!ancestors) { - return tags.th.defaultRole; - } - - /** - * @see https://www.w3.org/TR/html-aam-1.0/#el-th-gridcell - */ - if (hasGridParent(ancestors)) { - return 'gridcell'; + /** @see https://www.w3.org/TR/html-aam-1.0/#el-th-gridcell */ + if (hasGridParent(element, options?.ancestors)) { + return roles.gridcell; } - /** - * @see https://www.w3.org/TR/html-aam-1.0/#el-th - */ - const hasTableParent = !!firstMatchingAncestor([{ tagName: 'table', attributes: { role: 'table' } }], ancestors); - - if (hasTableParent) { - return 'cell'; + /** @see https://www.w3.org/TR/html-aam-1.0/#el-th */ + if (hasTableParent(element, options?.ancestors)) { + // Minor spec deviation: if inside a rowgroup, most browsers treat this as a columnheader + return hasRowgroupParent(element, options?.ancestors) ? roles.columnheader : roles.cell; } // See previous comment r.e. special behaviour. - return tags.th.defaultRole; + return NO_CORRESPONDING_ROLE; } diff --git a/src/types.d.ts b/src/types.d.ts index 1967da5..2d0d7af 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -146,8 +146,7 @@ export type TagName = // SVG extensions (partial) | 'g'; -export type Ancestor = HTMLElement | VirtualElement; -export type AncestorList = Ancestor[]; +export type VirtualAncestorList = VirtualElement[]; /** * 6.4 Translatable Attributes @@ -340,7 +339,8 @@ export type AbstractRole = | 'sectionhead' | 'select' | 'structure' - | 'widget'; + | 'widget' + | 'window'; /** * 5.3.2 Widget Roles @@ -422,6 +422,8 @@ export type DocumentStructureRole = | 'row' | 'rowgroup' | 'rowheader' + | 'sectionheader' + | 'sectionfooter' | 'separator' // (when NOT focusable) | 'strong' | 'subscript' diff --git a/test/dom/get-role.test.ts b/test/dom/get-role.test.ts new file mode 100644 index 0000000..c265b5c --- /dev/null +++ b/test/dom/get-role.test.ts @@ -0,0 +1,309 @@ +import { describe, expect, test } from 'vitest'; +import { NO_CORRESPONDING_ROLE, getRole, getTagName, tags } from '../../src/index.js'; +import { checkTestAndTagName, setUpDOM } from '../helpers.js'; + +describe('getRole', () => { + /** + * Document conformance requirements for use of aria-* attributes in HTML + * + * The following table provides normative per-element document conformance + * requirements for the use of ARIA markup in HTML documents. Additionally, it + * identifies the [implicit ARIA semantics](https://www.w3.org/TR/wai-aria-1.2/#implicit_semantics) that + * apply to [HTML elements](https://html.spec.whatwg.org/multipage/infrastructure.html#html-elements). + * The [implicit ARIA semantics](https://www.w3.org/TR/wai-aria-1.2/#implicit_semantics) of these + * elements are defined in [HTML AAM](https://www.w3.org/TR/html-aria/#bib-html-aam-1.0). + * @see https://www.w3.org/TR/html-aria/#docconformance + */ + const testCases: [ + string, + { + given: [string, string]; // [HTML, querySelector] + want: string | undefined; + }, + ][] = [ + ['a (no href)', { given: ['Link', 'a'], want: 'generic' }], + ['a (href)', { given: ['About', 'a'], want: 'link' }], + ['abbr', { given: ['DOM', 'abbr'], want: NO_CORRESPONDING_ROLE }], + ['address', { given: ['
    123 Address Ave.
    a', 'address'], want: 'group' }], + ['area (no href)', { given: ['Area', 'area'], want: 'generic' }], + ['area (href)', { given: ['About', 'area'], want: 'link' }], + ['article', { given: ['
    Article
    ', 'article'], want: 'article' }], + ['aside', { given: ['', 'aside'], want: 'complementary' }], + [ + 'aside (name, sectioning article)', + { given: ['
    ', 'aside'], want: 'complementary' }, + ], + [ + 'aside (name, sectioning aside)', + { given: ['', 'aside[aria-label]'], want: 'complementary' }, + ], + [ + 'aside (name, sectioning nav)', + { given: ['', 'aside'], want: 'complementary' }, + ], + [ + 'aside (name, sectioning section)', + { given: ['
    ', 'aside'], want: 'complementary' }, + ], + [ + 'aside (no name, sectioning article)', + { given: ['
    ', 'aside'], want: 'generic' }, + ], + [ + 'aside (no name, sectioning aside)', + { given: ['', 'aside aside'], want: 'generic' }, + ], + ['aside (no name, sectioning nav)', { given: ['', 'aside'], want: 'generic' }], + [ + 'aside (no name, sectioning section)', + { given: ['
    ', 'aside'], want: 'generic' }, + ], + ['audio', { given: ['', 'audio'], want: NO_CORRESPONDING_ROLE }], + ['b', { given: ['', 'b'], want: 'generic' }], + ['base', { given: ['', 'base'], want: NO_CORRESPONDING_ROLE }], + ['bdi', { given: ['', 'bdi'], want: 'generic' }], + ['bdo', { given: ['', 'bdo'], want: 'generic' }], + ['blockquote', { given: ['
    ', 'blockquote'], want: 'blockquote' }], + ['body', { given: ['', 'body'], want: 'generic' }], + ['br', { given: ['
    ', 'br'], want: NO_CORRESPONDING_ROLE }], + ['button', { given: ['', 'blockquote'], want: 'button' }], + ['canvas', { given: ['', 'canvas'], want: NO_CORRESPONDING_ROLE }], + ['caption', { given: ['', 'caption'], want: 'caption' }], + ['cite', { given: ['', 'cite'], want: NO_CORRESPONDING_ROLE }], + ['code', { given: ['', 'code'], want: 'code' }], + ['col', { given: ['
    ', 'col'], want: NO_CORRESPONDING_ROLE }], + ['colgroup', { given: ['
    ', 'colgroup'], want: NO_CORRESPONDING_ROLE }], + ['data', { given: ['', 'data'], want: 'generic' }], + ['datalist', { given: ['', 'datalist'], want: 'listbox' }], + ['dd', { given: ['
    ', 'dd'], want: NO_CORRESPONDING_ROLE }], + ['del', { given: ['', 'del'], want: 'deletion' }], + ['details', { given: ['
    ', 'details'], want: 'group' }], + ['dfn', { given: ['', 'dfn'], want: 'term' }], + ['dialog', { given: ['', 'dialog'], want: 'dialog' }], + ['div', { given: ['
    ', 'div'], want: 'generic' }], + ['dl', { given: ['
    ', 'dl'], want: NO_CORRESPONDING_ROLE }], + ['dt', { given: ['
    ', 'dt'], want: NO_CORRESPONDING_ROLE }], + ['em', { given: ['', 'em'], want: 'emphasis' }], + ['embed', { given: ['', 'embed'], want: NO_CORRESPONDING_ROLE }], + ['fieldset', { given: ['
    ', 'fieldset'], want: 'group' }], + ['figcaption', { given: ['
    ', 'figcaption'], want: NO_CORRESPONDING_ROLE }], + ['figure', { given: ['
    ', 'figure'], want: 'figure' }], + ['footer', { given: ['
    ', 'footer'], want: 'contentinfo' }], + ['footer (landmark)', { given: ['
    ', 'footer'], want: 'generic' }], + ['form', { given: ['
    ', 'form'], want: 'form' }], + ['g', { given: ['', 'g'], want: NO_CORRESPONDING_ROLE }], + ['h1', { given: ['

    ', 'h1'], want: 'heading' }], + ['h2', { given: ['

    ', 'h2'], want: 'heading' }], + ['h3', { given: ['

    ', 'h3'], want: 'heading' }], + ['h4', { given: ['

    ', 'h4'], want: 'heading' }], + ['h5', { given: ['
    ', 'h5'], want: 'heading' }], + ['h6', { given: ['
    ', 'h6'], want: 'heading' }], + ['head', { given: ['', 'head'], want: NO_CORRESPONDING_ROLE }], + ['header', { given: ['
    ', 'header'], want: 'banner' }], + ['header (in landmark)', { given: ['
    ', 'header'], want: 'generic' }], + ['hgroup', { given: ['
    ', 'hgroup'], want: 'group' }], + ['hr', { given: ['
    ', 'hr'], want: 'separator' }], + ['html', { given: ['', 'html'], want: 'document' }], + ['i', { given: ['', 'i'], want: 'generic' }], + ['iframe', { given: ['', 'iframe'], want: NO_CORRESPONDING_ROLE }], + ['img (named by alt)', { given: ['My image', 'img'], want: 'img' }], + ['img (named by label)', { given: ['', 'img'], want: 'img' }], + ['img (named by labelledby)', { given: ['', 'img'], want: 'img' }], + ['img (no name)', { given: ['', 'img'], want: 'none' }], + ['input', { given: ['', 'input'], want: 'textbox' }], + ['input[type=button]', { given: ['', 'input'], want: 'button' }], + ['input[type=color]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=date]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + [ + 'input[type=datetime-local]', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['input[type=email]', { given: ['', 'input'], want: 'textbox' }], + ['input[type=file]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=hidden]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=month]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=number]', { given: ['', 'input'], want: 'spinbutton' }], + ['input[type=password]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=radio]', { given: ['', 'input'], want: 'radio' }], + ['input[type=range]', { given: ['', 'input'], want: 'slider' }], + ['input[type=reset]', { given: ['', 'input'], want: 'button' }], + ['input[type=search]', { given: ['', 'input'], want: 'searchbox' }], + ['input[type=submit]', { given: ['', 'input'], want: 'button' }], + ['input[type=tel]', { given: ['', 'input'], want: 'textbox' }], + ['input[type=text]', { given: ['', 'input'], want: 'textbox' }], + ['input[type=shrek]', { given: ['', 'input'], want: 'textbox' }], + ['input[type=time]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + ['input[type=url]', { given: ['', 'input'], want: 'textbox' }], + ['input[type=week]', { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }], + + // Note: for input lists, ONLY text, search, tel, url, email, and invalid + // should produce a combobox. Other lists are ignored. But we want to test + // all of them to guarantee this behavior is correct. + // @see https://www.w3.org/TR/html-aria/#el-input-text-list + ['input (list)', { given: ['', 'input'], want: 'combobox' }], + ['input[type=button] (list)', { given: ['', 'input'], want: 'button' }], + [ + 'input[type=color] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + [ + 'input[type=date] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + [ + 'input[type=datetime-local] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['input[type=email] (list)', { given: ['', 'input'], want: 'combobox' }], + [ + 'input[type=file] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + [ + 'input[type=hidden] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + [ + 'input[type=month] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['input[type=number] (list)', { given: ['', 'input'], want: 'spinbutton' }], + [ + 'input[type=password] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['input[type=radio] (list)', { given: ['', 'input'], want: 'radio' }], + ['input[type=range] (list)', { given: ['', 'input'], want: 'slider' }], + ['input[type=reset] (list)', { given: ['', 'input'], want: 'button' }], + ['input[type=search] (list)', { given: ['', 'input'], want: 'combobox' }], + ['input[type=submit] (list)', { given: ['', 'input'], want: 'button' }], + ['input[type=tel] (list)', { given: ['', 'input'], want: 'combobox' }], + ['input[type=text] (list)', { given: ['', 'input'], want: 'combobox' }], + ['input[type=shrek] (list)', { given: ['', 'input'], want: 'combobox' }], + [ + 'input[type=time] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['input[type=url] (list)', { given: ['', 'input'], want: 'combobox' }], + [ + 'input[type=week] (list)', + { given: ['', 'input'], want: NO_CORRESPONDING_ROLE }, + ], + ['ins', { given: ['', 'ins'], want: 'insertion' }], + ['label', { given: ['', 'label'], want: NO_CORRESPONDING_ROLE }], + ['legend', { given: ['', 'legend'], want: NO_CORRESPONDING_ROLE }], + ['li', { given: ['
    ', 'li'], want: 'listitem' }], + ['li (no ancestors)', { given: ['
  • ', 'li'], want: 'generic' }], + ['link', { given: ['', 'link'], want: NO_CORRESPONDING_ROLE }], + ['kbd', { given: ['', 'kbd'], want: NO_CORRESPONDING_ROLE }], + ['main', { given: ['
    ', 'main'], want: 'main' }], + ['map', { given: ['', 'map'], want: NO_CORRESPONDING_ROLE }], + ['mark', { given: ['', 'mark'], want: 'mark' }], + ['math', { given: ['', 'math'], want: 'math' }], + ['menu', { given: ['', 'menu'], want: 'list' }], + ['meta', { given: ['', 'meta'], want: NO_CORRESPONDING_ROLE }], + ['meter', { given: ['', 'meter'], want: 'meter' }], + ['nav', { given: ['', 'nav'], want: 'navigation' }], + ['noscript', { given: ['', 'noscript'], want: NO_CORRESPONDING_ROLE }], + ['object', { given: ['', 'object'], want: NO_CORRESPONDING_ROLE }], + ['ol', { given: ['
      ', 'ol'], want: 'list' }], + ['optgroup', { given: ['', 'optgroup'], want: 'group' }], + ['option', { given: ['', 'option'], want: 'option' }], + ['output', { given: ['', 'output'], want: 'status' }], + ['p', { given: ['

      ', 'p'], want: 'paragraph' }], + ['picture', { given: ['', 'picture'], want: NO_CORRESPONDING_ROLE }], + ['pre', { given: ['
      ', 'pre'], want: 'generic' }],
      +    ['progress', { given: ['', 'progress'], want: 'progressbar' }],
      +    ['q', { given: ['', 'q'], want: 'generic' }],
      +    ['rp', { given: ['', 'rp'], want: NO_CORRESPONDING_ROLE }],
      +    ['rt', { given: ['', 'rt'], want: NO_CORRESPONDING_ROLE }],
      +    ['ruby', { given: ['', 'ruby'], want: NO_CORRESPONDING_ROLE }],
      +    ['s', { given: ['', 's'], want: 'deletion' }],
      +    ['samp', { given: ['', 'samp'], want: 'generic' }],
      +    ['script', { given: ['', 'script'], want: NO_CORRESPONDING_ROLE }],
      +    ['search', { given: ['', 'search'], want: 'search' }],
      +    ['section (named by label)', { given: ['
      ', 'section'], want: 'region' }], + ['section (named by labelledby)', { given: ['
      ', 'section'], want: 'region' }], + ['section (no name)', { given: ['
      ', 'section'], want: 'generic' }], + ['select', { given: ['', 'select'], want: 'combobox' }], + ['select[size=0]', { given: ['', 'select'], want: 'combobox' }], + ['select[size=1]', { given: ['', 'select'], want: 'combobox' }], + ['select[size=2]', { given: ['', 'select'], want: 'listbox' }], + ['select[multiple]', { given: ['', 'select'], want: 'listbox' }], + ['select[role=generic]', { given: ['', 'select'], want: 'generic' }], + ['span', { given: ['', 'span'], want: 'generic' }], + ['small', { given: ['', 'small'], want: 'generic' }], + ['source', { given: ['', 'source'], want: NO_CORRESPONDING_ROLE }], + ['slot', { given: ['', 'slot'], want: NO_CORRESPONDING_ROLE }], + ['strong', { given: ['', 'strong'], want: 'strong' }], + ['style', { given: ['', 'style'], want: NO_CORRESPONDING_ROLE }], + ['sub', { given: ['', 'sub'], want: 'subscript' }], + ['summary', { given: ['', 'summary'], want: NO_CORRESPONDING_ROLE }], + ['sup', { given: ['', 'sup'], want: 'superscript' }], + ['svg', { given: ['', 'svg'], want: 'graphics-document' }], + ['svg[role=img]', { given: ['', 'svg'], want: 'img' }], + [ + 'svg[role=graphics-symbol img]', + { given: ['', 'svg'], want: 'graphics-symbol' }, + ], + ['table', { given: ['
      ', 'table'], want: 'table' }], + ['tbody', { given: ['
      ', 'tbody'], want: 'rowgroup' }], + ['td', { given: ['
      ', 'td'], want: 'cell' }], + ['td', { given: ['
      ', 'td'], want: 'cell' }], + ['td (grid)', { given: ['
      ', 'td'], want: 'gridcell' }], + ['td (treegrid)', { given: ['
      ', 'td'], want: 'gridcell' }], + ['template', { given: ['', 'template'], want: NO_CORRESPONDING_ROLE }], + ['textarea', { given: ['', 'textarea'], want: 'textbox' }], + ['thead', { given: ['
      ', 'thead'], want: 'rowgroup' }], + ['tfoot', { given: ['
      ', 'tfoot'], want: 'rowgroup' }], + ['th', { given: ['
      ', 'th'], want: 'cell' }], + ['th (in thead)', { given: ['
      ', 'th'], want: 'columnheader' }], + ['th[scope=col]', { given: ['
      ', 'th'], want: 'columnheader' }], + [ + 'th[scope=colgroup]', + { + given: ['
      ', 'th'], + want: 'columnheader', + }, + ], + ['th[scope=row]', { given: ['
      ', 'th'], want: 'rowheader' }], + [ + 'th[scope=rowgroup]', + { + given: ['
      ', 'th'], + want: 'rowheader', + }, + ], + ['th (row)', { given: ['
      ', 'th'], want: 'cell' }], + ['th (grid)', { given: ['
      ', 'th'], want: 'gridcell' }], + ['th (treegrid)', { given: ['
      ', 'th'], want: 'gridcell' }], + ['time', { given: ['', 'time'], want: 'time' }], + ['title', { given: ['', 'title'], want: NO_CORRESPONDING_ROLE }], + ['tr', { given: ['
      ', 'tr'], want: 'row' }], + ['track', { given: ['', 'track'], want: NO_CORRESPONDING_ROLE }], + ['u', { given: ['', 'u'], want: 'generic' }], + ['ul', { given: ['
        ', 'ul'], want: 'list' }], + ['var', { given: ['', 'var'], want: NO_CORRESPONDING_ROLE }], + ['video', { given: ['', 'video'], want: NO_CORRESPONDING_ROLE }], + ['wbr', { given: ['', 'wbr'], want: NO_CORRESPONDING_ROLE }], + ]; + + const testedTags = new Set(); + + test.each(testCases)('%s', (name, { given, want }) => { + const { element } = setUpDOM(...given); + const tagName = getTagName(element); + testedTags.add(tagName); + checkTestAndTagName(name, tagName); + expect(getRole(element)?.name).toBe(want); + }); + + test('all tags are tested', () => { + const allTags = Object.keys(tags); + for (const tag of allTags) { + if (!testedTags.has(tag)) { + console.warn(`Tag "${tag}" is not tested`); + } + } + }); +}); diff --git a/test/dom/get-supported-roles.test.ts b/test/dom/get-supported-roles.test.ts new file mode 100644 index 0000000..d55cd11 --- /dev/null +++ b/test/dom/get-supported-roles.test.ts @@ -0,0 +1,378 @@ +import { describe, expect, test } from 'vitest'; +import { ALL_ROLES, NO_ROLES, getSupportedRoles, getTagName, isSupportedRole, tags } from '../../src/index.js'; +import { checkTestAndTagName, setUpDOM } from '../helpers.js'; + +describe('getSupportedRoles', () => { + const tests: [ + string, + { + given: [string, string]; // HTML, querySelector + want: ReturnType; + }, + ][] = [ + ['a (no href)', { given: ['', 'a'], want: ALL_ROLES }], + ['a[href=""]', { given: ['', 'a'], want: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'] }], // biome-ignore format: long list + ['a[href=#url]', { given: ['','a'], want: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'] }], // biome-ignore format: long list + ['address', { given: ['
        ', 'address'], want: ALL_ROLES }], + ['abbr', { given: ['', 'abbr'], want: ALL_ROLES }], + ['area (no href)', { given: ['', 'area'], want: ['button', 'generic', 'link'] }], + ['area[href=""]', { given: ['', 'area'], want: ['link'] }], + ['area[href=#url]', { given: ['', 'area'], want: ['link'] }], + [ + 'article', + { + given: ['
        ', 'article'], + want: ['article', 'application', 'document', 'feed', 'main', 'none', 'presentation', 'region'], // biome-ignore format: long list + }, + ], + [ + 'aside', + { + given: ['', 'aside'], + want: ['complementary', 'feed', 'none', 'note', 'presentation', 'region', 'search'], + }, + ], + ['audio', { given: ['', 'audio'], want: ['application'] }], + ['b', { given: ['', 'b'], want: ALL_ROLES }], + ['base', { given: ['', 'base'], want: NO_ROLES }], + ['bdi', { given: ['', 'bdi'], want: ALL_ROLES }], + ['bdo', { given: ['', 'bdo'], want: ALL_ROLES }], + ['br', { given: ['

        ', 'br'], want: ['none', 'presentation'] }], + ['blockquote', { given: ['
        ', 'blockquote'], want: ALL_ROLES }], + ['body', { given: ['', 'body'], want: ['generic'] }], + [ + 'button', + { + given: ['', 'button'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['caption', { given: ['
        ', 'caption'], want: ['caption'] }], + ['canvas', { given: ['', 'canvas'], want: ALL_ROLES }], + ['cite', { given: ['', 'cite'], want: ALL_ROLES }], + ['code', { given: ['', 'code'], want: ALL_ROLES }], + ['col', { given: ['
        ', 'col'], want: NO_ROLES }], + ['colgroup', { given: ['
        ', 'colgroup'], want: NO_ROLES }], + ['dd', { given: ['
        ', 'dd'], want: NO_ROLES }], + ['data', { given: ['', 'data'], want: ALL_ROLES }], + ['datalist', { given: ['', 'datalist'], want: ['listbox'] }], + ['del', { given: ['', 'del'], want: ALL_ROLES }], + ['details', { given: ['
        ', 'details'], want: ['group'] }], + ['dfn', { given: ['', 'dfn'], want: ALL_ROLES }], + ['dialog', { given: ['', 'dialog'], want: ['alertdialog', 'dialog'] }], + ['div', { given: ['
        ', 'div'], want: ALL_ROLES }], + ['div (dl)', { given: ['
        ', 'div'], want: ['none', 'presentation'] }], + ['dl', { given: ['
        ', 'dl'], want: ['group', 'list', 'none', 'presentation'] }], + ['dt', { given: ['
        ', 'dt'], want: ['listitem'] }], + ['em', { given: ['', 'em'], want: ALL_ROLES }], + [ + 'embed', + { + given: ['', 'embed'], + want: ['application', 'document', 'img', 'image', 'none', 'presentation'], + }, + ], + ['form', { given: ['
        ', 'form'], want: ['form', 'none', 'presentation', 'search'] }], + [ + 'fieldset', + { given: ['
        ', 'fieldset'], want: ['group', 'none', 'presentation', 'radiogroup'] }, + ], + ['figure', { given: ['
        ', 'figure'], want: ALL_ROLES }], + ['figcaption', { given: ['
        ', 'figcaption'], want: ['group', 'none', 'presentation'] }], + [ + 'footer (landmark)', + { given: ['
        ', 'footer'], want: ['generic', 'group', 'none', 'presentation'] }, + ], + [ + 'footer (default)', + { given: ['
        ', 'footer'], want: ['contentinfo', 'generic', 'group', 'none', 'presentation'] }, + ], + ['g', { given: ['', 'g'], want: ['group', 'graphics-object'] }], + ['h1', { given: ['

        ', 'h1'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['h2', { given: ['

        ', 'h2'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['h3', { given: ['

        ', 'h3'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['h4', { given: ['

        ', 'h4'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['h5', { given: ['
        ', 'h5'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['h6', { given: ['
        ', 'h6'], want: ['heading', 'none', 'presentation', 'tab'] }], + ['head', { given: ['', 'head'], want: NO_ROLES }], + [ + 'header (landmark)', + { given: ['
        ', 'header'], want: ['generic', 'group', 'none', 'presentation'] }, + ], + [ + 'header (default)', + { given: ['
        ', 'header'], want: ['banner', 'generic', 'group', 'none', 'presentation'] }, + ], + ['hgroup', { given: ['
        ', 'hgroup'], want: ALL_ROLES }], + ['html', { given: ['', 'html'], want: ['document'] }], + ['hr', { given: ['
        ', 'hr'], want: ['none', 'presentation', 'separator'] }], + ['i', { given: ['', 'i'], want: ALL_ROLES }], + [ + 'iframe', + { + given: ['', 'iframe'], + want: ['application', 'document', 'img', 'image', 'none', 'presentation'], + }, + ], + [ + 'img (name)', + { + given: ['Alternate text', 'img'], + want: ['button', 'checkbox', 'image', 'img', 'link', 'math', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option', 'progressbar', 'radio', 'scrollbar', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['img (no name)', { given: ['', 'img'], want: ['img', 'image', 'none', 'presentation'] }], + ['input', { given: ['', 'input'], want: ['combobox', 'searchbox', 'spinbutton', 'textbox'] }], + [ + 'input[type=button]', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + [ + 'input[type=checkbox]', + { given: ['', 'input'], want: ['checkbox', 'menuitemcheckbox', 'option', 'switch'] }, + ], + [ + 'input[type=checkbox] (pressed)', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'menuitemcheckbox', 'option', 'switch'], + }, + ], + ['input[type=color]', { given: ['', 'input'], want: [] }], + ['input[type=date]', { given: ['', 'input'], want: [] }], + ['input[type=datetime-local]', { given: ['', 'input'], want: [] }], + ['input[type=email]', { given: ['', 'input'], want: ['textbox'] }], + ['input[type=file]', { given: ['', 'input'], want: [] }], + ['input[type=hidden]', { given: ['', 'input'], want: [] }], + ['input[type=month]', { given: ['', 'input'], want: [] }], + ['input[type=number]', { given: ['', 'input'], want: ['spinbutton'] }], + ['input[type=range]', { given: ['', 'input'], want: ['slider'] }], + ['input[type=password]', { given: ['', 'input'], want: [] }], + ['input[type=radio]', { given: ['', 'input'], want: ['menuitemradio', 'radio'] }], + ['input[type=range]', { given: ['', 'input'], want: ['slider'] }], + [ + 'input[type=reset]', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['input[type=search]', { given: ['', 'input'], want: ['searchbox'] }], + [ + 'input[type=submit]', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['input[type=tel]', { given: ['', 'input'], want: ['textbox'] }], + [ + 'input[type=text]', + { + given: ['', 'input'], + want: ['combobox', 'searchbox', 'spinbutton', 'textbox'], + }, + ], + [ + 'input[type=shrek]', + { + given: ['', 'input'], + want: ['combobox', 'searchbox', 'spinbutton', 'textbox'], + }, + ], + ['input[type=time]', { given: ['', 'input'], want: [] }], + ['input[type=url]', { given: ['', 'input'], want: ['textbox'] }], + ['input[type=week]', { given: ['', 'input'], want: [] }], + ['input (list)', { given: ['', 'input'], want: ['combobox'] }], + [ + 'input[type=button] (list)', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + [ + 'input[type=checkbox] (list)', + { + given: ['', 'input'], + want: ['checkbox', 'menuitemcheckbox', 'option', 'switch'], + }, + ], + [ + 'input[type=checkbox] (list, pressed)', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'menuitemcheckbox', 'option', 'switch'], + }, + ], + ['input[type=color] (list)', { given: ['', 'input'], want: [] }], + ['input[type=date] (list)', { given: ['', 'input'], want: [] }], + [ + 'input[type=datetime-local] (list)', + { given: ['', 'input'], want: [] }, + ], + ['input[type=email] (list)', { given: ['', 'input'], want: ['combobox'] }], + ['input[type=file] (list)', { given: ['', 'input'], want: [] }], + ['input[type=hidden] (list)', { given: ['', 'input'], want: [] }], + ['input[type=month] (list)', { given: ['', 'input'], want: [] }], + ['input[type=number] (list)', { given: ['', 'input'], want: ['spinbutton'] }], + ['input[type=range] (list)', { given: ['', 'input'], want: ['slider'] }], + ['input[type=password] (list)', { given: ['', 'input'], want: [] }], + [ + 'input[type=radio]', + { given: ['', 'input'], want: ['menuitemradio', 'radio'] }, + ], + ['input[type=range] (list)', { given: ['', 'input'], want: ['slider'] }], + [ + 'input[type=reset]', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['input[type=search] (list)', { given: ['', 'input'], want: ['combobox'] }], + [ + 'input[type=submit]', + { + given: ['', 'input'], + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + }, + ], + ['input[type=tel] (list)', { given: ['', 'input'], want: ['combobox'] }], + ['input[type=text] (list)', { given: ['', 'input'], want: ['combobox'] }], + ['input[type=shrek] (list)', { given: ['', 'input'], want: ['combobox'] }], + ['input[type=time] (list)', { given: ['', 'input'], want: [] }], + ['input[type=url] (list)', { given: ['', 'input'], want: ['combobox'] }], + ['input[type=week] (list)', { given: ['', 'input'], want: [] }], + ['ins', { given: ['', 'ins'], want: ALL_ROLES }], + ['label', { given: ['', 'label'], want: [] }], + ['legend', { given: ['', 'legend'], want: [] }], + ['li', { given: ['
      • ', 'li'], want: ALL_ROLES }], + ['li (list parent)', { given: ['
        ', 'li'], want: ['listitem'] }], + ['link', { given: ['', 'link'], want: [] }], + ['kbd', { given: ['', 'kbd'], want: ALL_ROLES }], + ['main', { given: ['
        ', 'main'], want: ['main'] }], + ['mark', { given: ['', 'mark'], want: ALL_ROLES }], + ['math', { given: ['', 'math'], want: ['math'] }], + ['map', { given: ['', 'map'], want: [] }], + [ + 'menu', + { + given: ['', 'menu'], + want: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + }, + ], + ['meta', { given: ['', 'meta'], want: [] }], + ['meter', { given: ['', 'meter'], want: ['meter'] }], + [ + 'nav', + { given: ['', 'nav'], want: ['menu', 'menubar', 'navigation', 'none', 'presentation', 'tablist'] }, + ], + ['noscript', { given: ['', 'noscript'], want: [] }], + ['object', { given: ['', 'object'], want: ['application', 'document', 'img', 'image'] }], + [ + 'ol', + { + given: ['
          ', 'ol'], + want: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + }, + ], + ['optgroup', { given: ['', 'optgroup'], want: ['group'] }], + ['option', { given: ['', 'option'], want: ['option'] }], + ['output', { given: ['', 'output'], want: ALL_ROLES }], + ['p', { given: ['

          ', 'p'], want: ALL_ROLES }], + ['picture', { given: ['', 'picture'], want: [] }], + ['pre', { given: ['
          ', 'pre'], want: ALL_ROLES }],
          +    ['progress', { given: ['', 'progress'], want: ['progressbar'] }],
          +    ['q', { given: ['', 'q'], want: ALL_ROLES }],
          +    ['rp', { given: ['', 'rp'], want: ALL_ROLES }],
          +    ['rt', { given: ['', 'rt'], want: ALL_ROLES }],
          +    ['ruby', { given: ['', 'ruby'], want: ALL_ROLES }],
          +    ['s', { given: ['', 's'], want: ALL_ROLES }],
          +    ['samp', { given: ['', 'samp'], want: ALL_ROLES }],
          +    ['script', { given: ['', 'script'], want: [] }],
          +    [
          +      'search',
          +      { given: ['', 'search'], want: ['form', 'group', 'none', 'presentation', 'region', 'search'] },
          +    ],
          +    [
          +      'section',
          +      {
          +        given: ['
          ', 'section'], + want: ['alert', 'alertdialog', 'application', 'banner', 'complementary', 'contentinfo', 'dialog', 'document', 'feed', 'generic', 'group', 'log', 'main', 'marquee', 'navigation', 'none', 'note', 'presentation', 'region', 'search', 'status', 'tabpanel'], // biome-ignore format: long list + }, + ], + ['select', { given: ['', 'select'], want: ['combobox', 'menu'] }], + ['select[multiple]', { given: ['', 'select'], want: ['listbox'] }], + ['select[size=1]', { given: ['', 'select'], want: ['combobox', 'menu'] }], + ['select[size=2]', { given: ['', 'select'], want: ['listbox'] }], + // Note: roles are ignored for getSupportedRoles()! This is only testing the element itself. + ['select[role=generic]', { given: ['', 'select'], want: ['combobox', 'menu'] }], + ['slot', { given: ['', 'slot'], want: [] }], + ['small', { given: ['', 'small'], want: ALL_ROLES }], + ['source', { given: ['', 'source'], want: [] }], + ['span', { given: ['', 'span'], want: ALL_ROLES }], + ['strong', { given: ['', 'strong'], want: ALL_ROLES }], + ['style', { given: ['', 'style'], want: [] }], + ['sub', { given: ['', 'sub'], want: ALL_ROLES }], + ['summary', { given: ['', 'summary'], want: ALL_ROLES }], + ['summary (in details)', { given: ['
          ', 'summary'], want: [] }], + ['sup', { given: ['', 'sup'], want: ALL_ROLES }], + ['svg', { given: ['', 'svg'], want: ALL_ROLES }], + ['table', { given: ['
          ', 'table'], want: ALL_ROLES }], + ['tbody', { given: ['
          ', 'tbody'], want: ALL_ROLES }], + ['td', { given: ['
          ', 'td'], want: ['cell'] }], + ['td (grid)', { given: ['
          ', 'td'], want: ['gridcell'] }], + ['td (treegrid)', { given: ['
          ', 'td'], want: ['gridcell'] }], + ['thead', { given: ['
          ', 'thead'], want: ALL_ROLES }], + ['template', { given: ['', 'template'], want: [] }], + ['textarea', { given: ['', 'textarea'], want: ['textbox'] }], + ['tfoot', { given: ['
          ', 'tfoot'], want: ALL_ROLES }], + [ + 'th', + { given: ['
          ', 'th'], want: ['cell', 'columnheader', 'gridcell', 'rowheader'] }, + ], + ['time', { given: ['', 'time'], want: ALL_ROLES }], + ['title', { given: ['', 'title'], want: [] }], + ['tr', { given: ['
          ', 'tr'], want: ['row'] }], + ['track', { given: ['', 'track'], want: [] }], + ['u', { given: ['', 'u'], want: ALL_ROLES }], + [ + 'ul', + { + given: ['
            ', 'ul'], + want: ['group', 'list', 'listbox', 'menu', 'menubar', 'none', 'presentation', 'radiogroup', 'tablist', 'toolbar', 'tree'], // biome-ignore format: long list + }, + ], + ['var', { given: ['', 'var'], want: ALL_ROLES }], + ['video', { given: ['', 'video'], want: ['application'] }], + ['wbr', { given: ['', 'wbr'], want: ['none', 'presentation'] }], + ]; + + const testedTags = new Set(); + + test.each(tests)('%s', (name, { given, want }) => { + const { element } = setUpDOM(...given); + const tagName = getTagName(element); + testedTags.add(tagName); + expect(getSupportedRoles(element)).toEqual(want); + checkTestAndTagName(name, tagName); + }); + + test('all tags are tested', () => { + const allTags = Object.keys(tags); + for (const tag of allTags) { + if (!testedTags.has(tag)) { + console.warn(`Tag "${tag}" is not tested`); + } + } + }); +}); + +test('isSupportedRole', () => { + const { element } = setUpDOM('', 'html'); + expect(isSupportedRole('generic', element)).toBe(false); +}); diff --git a/test/dom/is-interactive.test.ts b/test/dom/is-interactive.test.ts new file mode 100644 index 0000000..b1eb523 --- /dev/null +++ b/test/dom/is-interactive.test.ts @@ -0,0 +1,217 @@ +import { describe, expect, test } from 'vitest'; +import { getTagName, isInteractive, tags } from '../../src/index.js'; +import { checkTestAndTagName, setUpDOM } from './../helpers'; + +// add
            +// add
            + +describe('isInteractive', () => { + // Note: HTML must only have a single root element for these tests + const tests: [ + string, + { + given: [string, string]; // [HTML, querySelector] + want: ReturnType; + }, + ][] = [ + ['a (no href)', { given: ['', 'a'], want: false }], + ['a (href)', { given: ['', 'a'], want: true }], + ['area (no href)', { given: ['', 'area'], want: false }], + ['area (href)', { given: ['', 'area'], want: true }], + ['abbr', { given: ['', 'abbr'], want: false }], + ['address', { given: ['
            ', 'address'], want: false }], + ['article', { given: ['
            ', 'article'], want: false }], + ['aside', { given: ['', 'aside'], want: false }], + ['audio', { given: ['', 'audio'], want: false }], + ['b', { given: ['', 'b'], want: false }], + ['base', { given: ['', 'base'], want: false }], + ['bdi', { given: ['', 'bdi'], want: false }], + ['bdo', { given: ['', 'bdo'], want: false }], + ['blockquote', { given: ['
            ', 'blockquote'], want: false }], + ['body', { given: ['', 'body'], want: false }], + ['br', { given: ['
            ', 'br'], want: false }], + ['button', { given: ['', 'button'], want: true }], + ['button[disabled]', { given: ['', 'button'], want: false }], + ['button[aria-disabled]', { given: ['', 'button'], want: false }], + ['canvas', { given: ['', 'canvas'], want: false }], + ['caption', { given: ['
            ', 'caption'], want: false }], + ['cite', { given: ['', 'cite'], want: false }], + ['code', { given: ['', 'code'], want: false }], + ['col', { given: ['
            ', 'col'], want: false }], + ['colgroup', { given: ['
            ', 'colgroup'], want: false }], + ['data', { given: ['', 'data'], want: false }], + ['datalist', { given: ['', 'datalist'], want: true }], + ['dd', { given: ['
            ', 'dd'], want: false }], + ['del', { given: ['', 'del'], want: false }], + ['details', { given: ['
            ', 'details'], want: false }], + ['dfn', { given: ['', 'dfn'], want: false }], + ['dialog', { given: ['', 'dialog'], want: true }], + ['div', { given: ['
            ', 'div'], want: false }], + ['div[role=button]', { given: ['
            ', 'div'], want: false }], + ['div[tabindex=0]', { given: ['
            ', 'div'], want: true }], + ['div[role=button,tabindex=0]', { given: ['
            ', 'div'], want: false }], + ['dl', { given: ['
            ', 'dl'], want: false }], + ['dt', { given: ['
            ', 'dt'], want: false }], + ['em', { given: ['', 'em'], want: false }], + ['embed', { given: ['', 'embed'], want: false }], + ['fieldset', { given: ['
            ', 'fieldset'], want: false }], + ['figcaption', { given: ['
            ', 'figcaption'], want: false }], + ['figure', { given: ['
            ', 'figure'], want: false }], + ['form', { given: ['
            ', 'form'], want: false }], + ['footer', { given: ['
            ', 'footer'], want: false }], + ['g', { given: ['', 'g'], want: false }], + ['h1', { given: ['

            ', 'h1'], want: false }], + ['h2', { given: ['

            ', 'h2'], want: false }], + ['h3', { given: ['

            ', 'h3'], want: false }], + ['h4', { given: ['

            ', 'h4'], want: false }], + ['h5', { given: ['
            ', 'h5'], want: false }], + ['h6', { given: ['
            ', 'h6'], want: false }], + ['head', { given: ['', 'head'], want: false }], + ['header', { given: ['
            ', 'header'], want: false }], + ['hgroup', { given: ['
            ', 'hgroup'], want: false }], + ['hr', { given: ['
            ', 'hr'], want: false }], + ['hr[tabindex=0]', { given: ['
            ', 'hr'], want: false }], + ['hr[aria-valuenow=10]', { given: ['
            ', 'hr'], want: false }], + ['hr[tabindex=0,aria-valuenow=10]', { given: ['
            ', 'hr'], want: true }], + ['html', { given: ['', 'html'], want: false }], + ['i', { given: ['', 'i'], want: false }], + ['iframe', { given: ['', 'iframe'], want: false }], + ['img', { given: ['', 'img'], want: false }], + ['input[type=button]', { given: ['', 'input'], want: true }], + ['input[type=checkbox]', { given: ['', 'input'], want: true }], + ['input[type=color]', { given: ['', 'input'], want: true }], + ['input[type=date]', { given: ['', 'input'], want: true }], + ['input[type=datetime-local]', { given: ['', 'input'], want: true }], + ['input[type=email]', { given: ['', 'input'], want: true }], + ['input[type=file]', { given: ['', 'input'], want: true }], + ['input[type=hidden]', { given: ['', 'input'], want: false }], + ['input[type=image]', { given: ['', 'input'], want: true }], + ['input[type=month]', { given: ['', 'input'], want: true }], + ['input[type=number]', { given: ['', 'input'], want: true }], + ['input[type=password]', { given: ['', 'input'], want: true }], + ['input[type=radio]', { given: ['', 'input'], want: true }], + ['input[type=range]', { given: ['', 'input'], want: true }], + ['input[type=reset]', { given: ['', 'input'], want: true }], + ['input[type=search]', { given: ['', 'input'], want: true }], + ['input[type=shrek]', { given: ['', 'input'], want: true }], + ['input[type=submit]', { given: ['', 'input'], want: true }], + ['input[type=tel]', { given: ['', 'input'], want: true }], + ['input[type=text]', { given: ['', 'input'], want: true }], + ['input[type=text] (disabled)', { given: ['', 'input'], want: false }], + [ + 'input[type=text] (aria-disabled)', + { given: ['', 'input'], want: false }, + ], + ['input[type=time]', { given: ['', 'input'], want: true }], + ['input[type=url]', { given: ['', 'input'], want: true }], + ['input[type=week]', { given: ['', 'input'], want: true }], + ['input (list)', { given: ['', 'input'], want: true }], + ['input[type=email] (list)', { given: ['', 'input'], want: true }], + ['ins', { given: ['', 'ins'], want: false }], + ['kbd', { given: ['', 'kbd'], want: false }], + ['label', { given: ['', 'label'], want: false }], + ['legend', { given: ['', 'legend'], want: false }], + ['li', { given: ['
          • ', 'li'], want: false }], + ['link', { given: ['', 'link'], want: false }], + ['main', { given: ['
            ', 'main'], want: false }], + ['map', { given: ['', 'map'], want: false }], + ['mark', { given: ['', 'mark'], want: false }], + ['math', { given: ['', 'math'], want: false }], + ['menu', { given: ['', 'menu'], want: false }], + ['menu[role=tree]', { given: ['', 'menu'], want: false }], + ['menu[tabindex=0,role=tree]', { given: ['', 'menu'], want: true }], + ['meta', { given: ['', 'meta'], want: false }], + ['meter', { given: ['', 'meter'], want: false }], + ['nav', { given: ['', 'nav'], want: false }], + ['noscript', { given: ['', 'noscript'], want: false }], + ['object', { given: ['', 'object'], want: false }], + ['ol', { given: ['
              ', 'ol'], want: false }], + ['optgroup', { given: ['', 'optgroup'], want: false }], + ['option', { given: ['', 'option'], want: true }], + ['output', { given: ['', 'output'], want: false }], + ['p', { given: ['

              ', 'p'], want: false }], + ['picture', { given: ['', 'picture'], want: false }], + ['pre', { given: ['
              ', 'pre'], want: false }],
              +    ['progress', { given: ['', 'progress'], want: true }],
              +    ['q', { given: ['', 'q'], want: false }],
              +    ['rp', { given: ['', 'rp'], want: false }],
              +    ['rt', { given: ['', 'rt'], want: false }],
              +    ['ruby', { given: ['', 'rudy'], want: false }],
              +    ['s', { given: ['', 's'], want: false }],
              +    ['samp', { given: ['', 'samp'], want: false }],
              +    ['script', { given: ['', 'script'], want: false }],
              +    ['search', { given: ['', 'search'], want: false }],
              +    ['section', { given: ['
              ', 'section'], want: false }], + ['select', { given: ['', 'select'], want: true }], + ['select[size=2]', { given: ['', 'select'], want: true }], + ['select[multiple]', { given: ['', 'select'], want: true }], + ['slot', { given: ['', 'slot'], want: false }], + ['small', { given: ['', 'small'], want: false }], + ['source', { given: ['', 'source'], want: false }], + ['span', { given: ['', 'span'], want: false }], + ['strong', { given: ['', 'strong'], want: false }], + ['style', { given: ['', 'style'], want: false }], + ['sub', { given: ['', 'sub'], want: false }], + ['summary', { given: ['', 'summary'], want: false }], + ['sup', { given: ['', 'sub'], want: false }], + ['svg', { given: ['', 'svg'], want: false }], + ['table', { given: ['
              ', 'table'], want: false }], + ['tbody', { given: ['
              ', 'tbody'], want: false }], + ['td', { given: ['
              ', 'td'], want: false }], + ['td[role=gridcell]', { given: ['
              ', 'td'], want: false }], + [ + 'td[tabindex=0,role=gridcell]', + { given: ['
              ', 'td'], want: true }, + ], + [ + 'td[tabindex=0] (grid descendant)', + { given: ['
              ', 'td'], want: true }, + ], + [ + 'td[tabindex=0] (treegrid descendant)', + { given: ['
              ', 'td'], want: true }, + ], + ['template', { given: ['', 'template'], want: false }], + ['textarea', { given: ['', 'textarea'], want: true }], + ['tfoot', { given: ['
              ', 'tfoot'], want: false }], + ['th', { given: ['
              ', 'th'], want: false }], + ['th', { given: ['
              ', 'th'], want: false }], + ['thead', { given: ['
              ', 'thead'], want: false }], + ['time', { given: ['', 'time'], want: false }], + ['title', { given: ['', 'title'], want: false }], + ['tr', { given: ['
              ', 'tr'], want: false }], + ['tr[tabindex=0]', { given: ['
              ', 'tr'], want: false }], + [ + 'tr[tabindex=0] (grid descendant)', + { given: ['
              ', 'tr'], want: false }, + ], + [ + 'tr[tabindex=0] (treegrid descendant)', + { given: ['
              ', 'tr'], want: false }, + ], + ['track', { given: ['', 'track'], want: false }], + ['u', { given: ['', 'u'], want: false }], + ['ul', { given: ['
                ', 'ul'], want: false }], + ['var', { given: ['', 'var'], want: false }], + ['video', { given: ['', 'video'], want: false }], + ['wbr', { given: ['', 'wbr'], want: false }], + ]; + + const testedTags = new Set(); + + test.each(tests)('%s', (name, { given, want }) => { + const { element } = setUpDOM(...given); + const tagName = getTagName(element); + checkTestAndTagName(name, tagName); + expect(isInteractive(element)).toBe(want); + }); + + // Vitest bug: this breaks? + // test('all tags are tested', () => { + // // verify all tags tested + // const allTags = Object.keys(tags); + // for (const tag of allTags) { + // expect(testedTags.has(tag), tag).toBe(true); + // } + // }); +}); diff --git a/test/helpers.ts b/test/helpers.ts index 2768b1b..620a30a 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -4,3 +4,28 @@ export function checkTestAndTagName(testName: string, tagName: string) { throw new Error(`Test "${testName}" is testing tag "${tagName}". Has there been a mistake?`); } } + +/** Ensure list is sorted alphabetically. */ +export function copyAndSortList(list: T[]): T[] { + return [...list].sort(); +} + +/** + * Create a DOM tree from an HTML string. + * Because these are small and a closed loop, we can not pollute the DOM by + * appending these; these are all virtual. If we do end up writing to the DOM, + * we would have to do more work isolating parallel tests AS WELL as come up + * with a strategy for testing `` and other structural tags. + */ +export function setUpDOM(html: string, querySelector: string) { + if (querySelector === 'html') { + return { root: document.documentElement, element: document.documentElement as Element }; + } + if (querySelector === 'head' || querySelector === 'body') { + return { root: document.documentElement, element: document.querySelector(querySelector) as Element }; + } + + const container = document.createElement('div'); + container.innerHTML = html; + return { root: container, element: (container.querySelector(querySelector) || container.children[0]) as Element }; +} diff --git a/test/data-integrity.test.ts b/test/node/data-integrity.test.ts similarity index 65% rename from test/data-integrity.test.ts rename to test/node/data-integrity.test.ts index ce7749c..d3fb59e 100644 --- a/test/data-integrity.test.ts +++ b/test/node/data-integrity.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { roles, tags } from '../src/index.js'; +import { roles, tags } from '../../src/index.js'; +import { copyAndSortList } from '../helpers.js'; // simple tests that check for simple errors and inconsistencies in the data. // even though the data is largely automatically generated from the spec, it’s @@ -20,12 +21,17 @@ describe('html data', () => { describe('role data', () => { for (const [role, roleData] of Object.entries(roles)) { describe(role, () => { + test('name', () => { + expect(roleData.name).toBe(role); + }); if (roleData.required.length) { test('required', () => { expect( roleData.required.every((a) => roleData.supported.includes(a)), 'supported aria-* attributes missing some required aria-* attributes', ).toBe(true); + const sorted = copyAndSortList(roleData.required); + expect(roleData.required, 'sorted').toEqual(sorted); }); } test('prohibited', () => { @@ -33,10 +39,21 @@ describe('role data', () => { roleData.prohibited.every((a) => !roleData.supported.includes(a)), 'prohibited aria-* attributes in supported aria-* attributes', ).toBe(true); + const sorted = copyAndSortList(roleData.prohibited); + expect(roleData.prohibited, 'sorted').toEqual(sorted); }); test('supported', () => { const deduped = new Set(roleData.supported); expect(deduped.size, 'duplicate attributes').toBe(roleData.supported.length); + const sorted = copyAndSortList(roleData.supported); + expect(roleData.supported, 'sorted').toEqual(sorted); + }); + + // this is a compiler optimization for monomorphism + test('Key order', () => { + const original = Object.keys(roleData); + const sorted = copyAndSortList(original); + expect(original).toEqual(sorted); }); }); } diff --git a/test/get-elements.test.ts b/test/node/get-elements.test.ts similarity index 96% rename from test/get-elements.test.ts rename to test/node/get-elements.test.ts index 9bf60f0..2e52a4e 100644 --- a/test/get-elements.test.ts +++ b/test/node/get-elements.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { getElements, roles } from '../src/index.js'; -import { checkTestAndTagName } from './helpers.js'; +import { getElements, roles } from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; describe('getElements', () => { const tests: [string, { given: Parameters[0]; want: ReturnType }][] = [ @@ -10,7 +10,7 @@ describe('getElements', () => { ['article', { given: 'article', want: [{ tagName: 'article' }] }], ['banner', { given: 'banner', want: [{ tagName: 'header' }] }], ['blockquote', { given: 'blockquote', want: [{ tagName: 'blockquote' }] }], - ['button', { given: 'button', want: [{ tagName: 'button', attributes: { type: 'button' } }] }], + ['button', { given: 'button', want: [{ tagName: 'button' }] }], ['caption', { given: 'caption', want: [{ tagName: 'caption' }] }], ['cell', { given: 'cell', want: [{ tagName: 'td' }] }], ['checkbox', { given: 'checkbox', want: [{ tagName: 'input', attributes: { type: 'checkbox' } }] }], @@ -101,6 +101,8 @@ describe('getElements', () => { ['row', { given: 'row', want: [{ tagName: 'tr' }] }], ['rowgroup', { given: 'rowgroup', want: [{ tagName: 'tbody' }, { tagName: 'tfoot' }, { tagName: 'thead' }] }], ['rowheader', { given: 'rowheader', want: [{ tagName: 'th', attributes: { scope: 'row' } }] }], + ['sectionfooter', { given: 'sectionfooter', want: undefined }], + ['sectionheader', { given: 'sectionheader', want: undefined }], ['scrollbar', { given: 'scrollbar', want: undefined }], ['search', { given: 'search', want: [{ tagName: 'search' }] }], ['searchbox', { given: 'searchbox', want: [{ tagName: 'input', attributes: { type: 'search' } }] }], diff --git a/test/get-required-attributes.test.ts b/test/node/get-required-attributes.test.ts similarity index 96% rename from test/get-required-attributes.test.ts rename to test/node/get-required-attributes.test.ts index e15524c..7e2a935 100644 --- a/test/get-required-attributes.test.ts +++ b/test/node/get-required-attributes.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { getRequiredAttributes, isRequiredAttribute, roles } from '../src'; -import { checkTestAndTagName } from './helpers'; +import { getRequiredAttributes, isRequiredAttribute, roles } from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; const tests: [ string, @@ -74,6 +74,8 @@ const tests: [ ['rowgroup', { given: ['rowgroup'], want: [] }], ['rowheader', { given: ['rowheader'], want: [] }], ['scrollbar', { given: ['scrollbar'], want: ['aria-controls', 'aria-valuenow'] }], + ['sectionheader', { given: ['sectionheader'], want: [] }], + ['sectionfooter', { given: ['sectionfooter'], want: [] }], ['search', { given: ['search'], want: [] }], ['searchbox', { given: ['searchbox'], want: [] }], // TODO: handle focusable? diff --git a/test/get-role.test.ts b/test/node/get-role.test.ts similarity index 89% rename from test/get-role.test.ts rename to test/node/get-role.test.ts index 3bd00f3..8c89503 100644 --- a/test/get-role.test.ts +++ b/test/node/get-role.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { type VirtualElement, getRole, tags } from '../src/index.js'; -import { checkTestAndTagName } from './helpers.js'; +import { NO_CORRESPONDING_ROLE, type VirtualElement, getRole, tags } from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; describe('getRole', () => { /** @@ -18,17 +18,15 @@ describe('getRole', () => { string, { given: Parameters; - want: ReturnType; + want: string | undefined; }, ][] = [ - ['a', { given: [{ tagName: 'a' }], want: 'link' }], + ['a', { given: [{ tagName: 'a' }], want: 'generic' }], ['a (href)', { given: [{ tagName: 'a', attributes: { href: '/about' } }], want: 'link' }], - ['a (no href)', { given: [{ tagName: 'a', attributes: {} }], want: 'generic' }], ['abbr', { given: [{ tagName: 'abbr' }], want: undefined }], ['address', { given: [{ tagName: 'address' }], want: 'group' }], - ['area', { given: [{ tagName: 'area' }], want: 'link' }], + ['area', { given: [{ tagName: 'area' }], want: 'generic' }], ['area (href)', { given: [{ tagName: 'area', attributes: { href: '/about' } }], want: 'link' }], - ['area (no href)', { given: [{ tagName: 'area', attributes: {} }], want: 'generic' }], ['article', { given: [{ tagName: 'article' }], want: 'article' }], ['aside', { given: [{ tagName: 'aside' }], want: 'complementary' }], [ @@ -253,6 +251,7 @@ describe('getRole', () => { ['legend', { given: [{ tagName: 'legend' }], want: undefined }], ['li', { given: [{ tagName: 'li' }], want: 'listitem' }], ['li (no ancestors)', { given: [{ tagName: 'li' }, { ancestors: [] }], want: 'generic' }], + ['li (list parent)', { given: [{ tagName: 'li' }, { ancestors: [{ tagName: 'ul' }] }], want: 'listitem' }], ['link', { given: [{ tagName: 'link' }], want: undefined }], ['kbd', { given: [{ tagName: 'kbd' }], want: undefined }], ['main', { given: [{ tagName: 'main' }], want: 'main' }], @@ -314,7 +313,7 @@ describe('getRole', () => { ['table', { given: [{ tagName: 'table' }], want: 'table' }], ['tbody', { given: [{ tagName: 'tbody' }], want: 'rowgroup' }], ['td', { given: [{ tagName: 'td' }], want: 'cell' }], - ['td (no ancestors)', { given: [{ tagName: 'td' }, { ancestors: [] }], want: undefined }], + ['td (no ancestors)', { given: [{ tagName: 'td' }, { ancestors: [] }], want: NO_CORRESPONDING_ROLE }], ['td (table)', { given: [{ tagName: 'td' }, { ancestors: [{ tagName: 'table' }] }], want: 'cell' }], [ 'td (grid)', @@ -330,17 +329,25 @@ describe('getRole', () => { want: 'gridcell', }, ], - ['template', { given: [{ tagName: 'template' }], want: undefined }], + ['template', { given: [{ tagName: 'template' }], want: NO_CORRESPONDING_ROLE }], ['textarea', { given: [{ tagName: 'textarea' }], want: 'textbox' }], ['thead', { given: [{ tagName: 'thead' }], want: 'rowgroup' }], ['tfoot', { given: [{ tagName: 'tfoot' }], want: 'rowgroup' }], - ['th', { given: [{ tagName: 'th' }], want: 'columnheader' }], - ['th (no ancestors)', { given: [{ tagName: 'th' }, { ancestors: [] }], want: undefined }], + ['th', { given: [{ tagName: 'th' }], want: 'cell' }], + ['th (no ancestors)', { given: [{ tagName: 'th' }, { ancestors: [] }], want: NO_CORRESPONDING_ROLE }], ['th[scope=col]', { given: [{ tagName: 'th', attributes: { scope: 'col' } }], want: 'columnheader' }], ['th[scope=colgroup]', { given: [{ tagName: 'th', attributes: { scope: 'colgroup' } }], want: 'columnheader' }], ['th[scope=row]', { given: [{ tagName: 'th', attributes: { scope: 'row' } }], want: 'rowheader' }], ['th[scope=rowgroup]', { given: [{ tagName: 'th', attributes: { scope: 'rowgroup' } }], want: 'rowheader' }], ['th (table)', { given: [{ tagName: 'th' }, { ancestors: [{ tagName: 'table' }] }], want: 'cell' }], + [ + 'th (thead)', + { given: [{ tagName: 'th' }, { ancestors: [{ tagName: 'thead' }, { tagName: 'table' }] }], want: 'columnheader' }, + ], + [ + 'th (row)', + { given: [{ tagName: 'th' }, { ancestors: [{ tagName: 'tr' }, { tagName: 'table' }] }], want: 'cell' }, + ], [ 'th (grid)', { @@ -366,50 +373,20 @@ describe('getRole', () => { ['wbr', { given: [{ tagName: 'wbr' }], want: undefined }], ]; - describe('from object', () => { - const testedTags = new Set(); - - test.each(testCases)('%s', (name, { given, want }) => { - testedTags.add(given[0].tagName); - checkTestAndTagName(name, given[0].tagName); - expect(getRole(...given)).toBe(want); - }); + const testedTags = new Set(); - test('all tags are tested', () => { - const allTags = Object.keys(tags); - for (const tag of allTags) { - if (!testedTags.has(tag)) { - console.warn(`Tag "${tag}" is not tested`); - } - } - }); + test.each(testCases)('%s', (name, { given, want }) => { + testedTags.add(given[0].tagName); + checkTestAndTagName(name, given[0].tagName); + expect(getRole(...given)?.name).toBe(want); }); - describe('from DOM element', () => { - function elFromVirtual(el: VirtualElement) { - const element = document.createElement(el.tagName); - if (el.attributes) { - for (const [name, value] of Object.entries(el.attributes)) { - element.setAttribute(name, String(value)); - } + test('all tags are tested', () => { + const allTags = Object.keys(tags); + for (const tag of allTags) { + if (!testedTags.has(tag)) { + console.warn(`Tag "${tag}" is not tested`); } - return element; } - - // Note: because of the way the DOM tests work, we can’t specify - // an empty attributes array, so 2 tests written in object - // syntax operate differently. Only skip those 2. - const domTestCases = testCases.filter(([name]) => !['a', 'area'].includes(name)); - - test.each(domTestCases)('%s', (_, { given, want }) => { - // convert main element to DOM element - const mainEl = elFromVirtual(given[0] as VirtualElement); - const options = { ...given[1] }; - // also, to test ancestors, convert those, too - if (options.ancestors) { - options.ancestors = options.ancestors.map((el) => elFromVirtual(el as VirtualElement)); - } - expect(getRole(mainEl, options)).toBe(want); - }); }); }); diff --git a/test/get-supported-attributes.test.ts b/test/node/get-supported-attributes.test.ts similarity index 96% rename from test/get-supported-attributes.test.ts rename to test/node/get-supported-attributes.test.ts index e3c803a..91d8972 100644 --- a/test/get-supported-attributes.test.ts +++ b/test/node/get-supported-attributes.test.ts @@ -10,8 +10,8 @@ import { removeProhibited, roles, tags, -} from '../src/index.js'; -import { checkTestAndTagName } from './helpers.js'; +} from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; // These tests are the most difficult to write—it’s very easy to make them // circular. To avoid that, it’s worth describing where the data came from. @@ -72,8 +72,16 @@ const tests: [ string, { given: Parameters; want: ReturnType }, ][] = [ - ['a', { given: [{ tagName: 'a' }], want: [...GLOBAL_ATTRIBUTES, ...roles.link.supported] }], - ['area', { given: [{ tagName: 'area' }], want: [...GLOBAL_ATTRIBUTES, ...roles.link.supported] }], + ['a', { given: [{ tagName: 'a' }], want: GENERIC_NO_NAMING }], + [ + 'a (href)', + { given: [{ tagName: 'a', attributes: { href: '#' } }], want: [...GLOBAL_ATTRIBUTES, ...roles.link.supported] }, + ], + ['area', { given: [{ tagName: 'area' }], want: GENERIC_NO_NAMING }], + [ + 'area (href)', + { given: [{ tagName: 'area', attributes: { href: '#' } }], want: [...GLOBAL_ATTRIBUTES, ...roles.link.supported] }, + ], ['abbr', { given: [{ tagName: 'abbr' }], want: removeProhibited(GLOBAL_ATTRIBUTES, { nameProhibited: true }) }], ['address', { given: [{ tagName: 'address' }], want: [...GLOBAL_ATTRIBUTES, ...roles.group.supported] }], ['article', { given: [{ tagName: 'article' }], want: [...GLOBAL_ATTRIBUTES, ...roles.article.supported] }], @@ -128,9 +136,7 @@ const tests: [ 'dfn', { given: [{ tagName: 'dfn' }], - want: removeProhibited([...GLOBAL_ATTRIBUTES, ...roles.term.supported], { - prohibited: ['aria-braillelabel', 'aria-label', 'aria-labelledby'], - }), + want: removeProhibited([...GLOBAL_ATTRIBUTES, ...roles.term.supported], { nameProhibited: true }), }, ], ['dialog', { given: [{ tagName: 'dialog' }], want: [...GLOBAL_ATTRIBUTES, ...roles.dialog.supported] }], @@ -474,7 +480,14 @@ const tests: [ ['template', { given: [{ tagName: 'template' }], want: NO_ATTRIBUTES }], ['textarea', { given: [{ tagName: 'textarea' }], want: TEXTBOX_ATTRIBUTES }], ['tfoot', { given: [{ tagName: 'tfoot' }], want: [...GLOBAL_ATTRIBUTES, ...roles.rowgroup.supported] }], - ['th', { given: [{ tagName: 'th' }], want: [...GLOBAL_ATTRIBUTES, ...roles.columnheader.supported] }], + ['th', { given: [{ tagName: 'th' }], want: [...GLOBAL_ATTRIBUTES, ...roles.cell.supported] }], + [ + 'th (thead)', + { + given: [{ tagName: 'th' }, { ancestors: [{ tagName: 'thead' }] }], + want: [...GLOBAL_ATTRIBUTES, ...roles.columnheader.supported], + }, + ], [ 'th', { @@ -519,13 +532,11 @@ describe('getSupportedAttributes', () => { describe('isSupportedAttribute', () => { const allAttributes = Object.keys(attributes) as ARIAAttribute[]; allAttributes.sort((a, b) => a.localeCompare(b)); - for (const [name, { given, want }] of tests) { - test(name, () => { - for (const attr of allAttributes) { - expect(isSupportedAttribute(attr, ...given)).toBe(want.includes(attr)); - } - }); - } + test.each(tests)('%s', (_, { given, want }) => { + for (const attr of allAttributes) { + expect(isSupportedAttribute(attr, ...given)).toBe(want.includes(attr)); + } + }); }); const valueTests: [ diff --git a/test/get-supported-roles.test.ts b/test/node/get-supported-roles.test.ts similarity index 91% rename from test/get-supported-roles.test.ts rename to test/node/get-supported-roles.test.ts index 90817d2..32dfe46 100644 --- a/test/get-supported-roles.test.ts +++ b/test/node/get-supported-roles.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { ALL_ROLES, NO_ROLES, getSupportedRoles, isSupportedRole, tags } from '../src/index.js'; -import { checkTestAndTagName } from './helpers.js'; +import { ALL_ROLES, NO_ROLES, getSupportedRoles, isSupportedRole, tags } from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; describe('getSupportedRoles', () => { const tests: [ @@ -10,18 +10,14 @@ describe('getSupportedRoles', () => { want: ReturnType; }, ][] = [ - [ - 'a', - { - given: [{ tagName: 'a' }], - want: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list - }, - ], - ['a (no href)', { given: [{ tagName: 'a', attributes: {} }], want: ALL_ROLES }], + ['a (no href)', { given: [{ tagName: 'a' }], want: ALL_ROLES }], + ['a[href=""]', { given: [{ tagName: 'a', attributes: { href: '' } }], want: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'] }], // biome-ignore format: long list + ['a[href=#url]', { given: [{ tagName: 'a', attributes: { href: '#url' } }], want: ['button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'switch', 'tab', 'treeitem'] }], // biome-ignore format: long list ['address', { given: [{ tagName: 'address' }], want: ALL_ROLES }], ['abbr', { given: [{ tagName: 'abbr' }], want: ALL_ROLES }], - ['area', { given: [{ tagName: 'area' }], want: ['link'] }], - ['area (no href)', { given: [{ tagName: 'area', attributes: {} }], want: ['button', 'generic', 'link'] }], + ['area (no href)', { given: [{ tagName: 'area' }], want: ['button', 'generic', 'link'] }], + ['area[href=""]', { given: [{ tagName: 'area', attributes: { href: '' } }], want: ['link'] }], + ['area[href=#url]', { given: [{ tagName: 'area', attributes: { href: '#url' } }], want: ['link'] }], [ 'article', { @@ -48,7 +44,7 @@ describe('getSupportedRoles', () => { 'button', { given: [{ tagName: 'button' }], - want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list }, ], ['caption', { given: [{ tagName: 'caption' }], want: ['caption'] }], @@ -128,7 +124,7 @@ describe('getSupportedRoles', () => { 'input[type=button]', { given: [{ tagName: 'input', attributes: { type: 'button' } }], - want:['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list }, ], [ @@ -198,7 +194,7 @@ describe('getSupportedRoles', () => { 'input[type=button] (list)', { given: [{ tagName: 'input', attributes: { type: 'button', list: 'buttons' } }], - want:['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list + want: ['button', 'checkbox', 'combobox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'separator', 'slider', 'switch', 'tab', 'treeitem'], // biome-ignore format: long list }, ], [ @@ -313,6 +309,7 @@ describe('getSupportedRoles', () => { ['legend', { given: [{ tagName: 'legend' }], want: [] }], ['li', { given: [{ tagName: 'li' }], want: ['listitem'] }], ['li (no ancestors)', { given: [{ tagName: 'li' }, { ancestors: [] }], want: ALL_ROLES }], + ['li (list parent)', { given: [{ tagName: 'li' }, { ancestors: [{ tagName: 'ul' }] }], want: ['listitem'] }], ['link', { given: [{ tagName: 'link' }], want: [] }], ['kbd', { given: [{ tagName: 'kbd' }], want: ALL_ROLES }], ['main', { given: [{ tagName: 'main' }], want: ['main'] }], @@ -386,8 +383,8 @@ describe('getSupportedRoles', () => { ['table', { given: [{ tagName: 'table' }], want: ALL_ROLES }], ['tbody', { given: [{ tagName: 'tbody' }], want: ALL_ROLES }], ['td', { given: [{ tagName: 'td' }], want: ['cell'] }], - ['td (table)', { given: [{ tagName: 'td' }, { ancestors: [{ tagName: 'table' }] }], want: ['cell'] }], ['td (no ancestors)', { given: [{ tagName: 'td' }, { ancestors: [] }], want: ALL_ROLES }], + ['td (table)', { given: [{ tagName: 'td' }, { ancestors: [{ tagName: 'table' }] }], want: ['cell'] }], [ 'td (grid)', { @@ -412,7 +409,8 @@ describe('getSupportedRoles', () => { ['title', { given: [{ tagName: 'title' }], want: [] }], ['tr', { given: [{ tagName: 'tr' }], want: ['row'] }], ['tr (no ancestors)', { given: [{ tagName: 'tr' }, { ancestors: [] }], want: ALL_ROLES }], - ['track', { given: [{ tagName: 'track' }, { ancestors: [] }], want: [] }], + ['tr (table)', { given: [{ tagName: 'tr' }, { ancestors: [{ tagName: 'table' }] }], want: ['row'] }], + ['track', { given: [{ tagName: 'track' }], want: [] }], ['u', { given: [{ tagName: 'u' }], want: ALL_ROLES }], [ 'ul', diff --git a/test/html-aria.bench.ts b/test/node/html-aria.bench.ts similarity index 55% rename from test/html-aria.bench.ts rename to test/node/html-aria.bench.ts index d9a8b7f..ac344f7 100644 --- a/test/html-aria.bench.ts +++ b/test/node/html-aria.bench.ts @@ -1,7 +1,7 @@ import { elementRoles } from 'aria-query'; import { getRole as domAccessibilityApiGetRole } from 'dom-accessibility-api'; import { bench, describe } from 'vitest'; -import { getRole } from '../src/index.js'; +import { getRole } from '../../src/index.js'; describe('getRole (virtual)', () => { bench('html-aria', () => { @@ -16,15 +16,17 @@ describe('getRole (virtual)', () => { }); }); -describe('getRole (HTMLElement)', () => { - const element = document.createElement('input'); - element.type = 'checkbox'; +if (typeof document !== 'undefined') { + describe('getRole (HTMLElement)', () => { + const element = document.createElement('input'); + element.type = 'checkbox'; - bench('html-aria', () => { - getRole(element); - }); + bench('html-aria', () => { + getRole(element); + }); - bench('dom-accessibility-api', () => { - domAccessibilityApiGetRole(element); + bench('dom-accessibility-api', () => { + domAccessibilityApiGetRole(element); + }); }); -}); +} diff --git a/test/is-interactive.test.ts b/test/node/is-interactive.test.ts similarity index 96% rename from test/is-interactive.test.ts rename to test/node/is-interactive.test.ts index 4779f03..06c3239 100644 --- a/test/is-interactive.test.ts +++ b/test/node/is-interactive.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { isInteractive, tags } from '../src/index.js'; -import { checkTestAndTagName } from './helpers'; +import { isInteractive, tags } from '../../src/index.js'; +import { checkTestAndTagName } from './../helpers'; // add
                // add
                @@ -8,9 +8,9 @@ import { checkTestAndTagName } from './helpers'; describe('isInteractive', () => { const tests: [string, { given: Parameters; want: ReturnType }][] = [ ['a', { given: [{ tagName: 'a' }], want: false }], - ['a', { given: [{ tagName: 'a', attributes: { href: '#' } }], want: true }], - ['area', { given: [{ tagName: 'area' }], want: false }], - ['area', { given: [{ tagName: 'area', attributes: { href: '#' } }], want: true }], + ['a[ref=#]', { given: [{ tagName: 'a', attributes: { href: '#' } }], want: true }], + ['area (no href)', { given: [{ tagName: 'area' }], want: false }], + ['area[href=#]', { given: [{ tagName: 'area', attributes: { href: '#' } }], want: true }], ['abbr', { given: [{ tagName: 'abbr' }], want: false }], ['address', { given: [{ tagName: 'address' }], want: false }], ['article', { given: [{ tagName: 'article' }], want: false }], @@ -231,12 +231,10 @@ describe('isInteractive', () => { expect(isInteractive(...given)).toBe(want); }); - test('all tags tested', () => { + test('all tags are tested', () => { const allTags = Object.keys(tags); for (const tag of allTags) { - if (!testedTags.has(tag)) { - console.warn(`Tag "${tag}" is not tested`); - } + expect(testedTags.has(tag)).toBe(true); } }); }); diff --git a/test/is-name-required.test.ts b/test/node/is-name-required.test.ts similarity index 97% rename from test/is-name-required.test.ts rename to test/node/is-name-required.test.ts index 15cebe3..01cdd98 100644 --- a/test/is-name-required.test.ts +++ b/test/node/is-name-required.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { isNameRequired, roles } from '../src/index.js'; -import { checkTestAndTagName } from './helpers.js'; +import { isNameRequired, roles } from '../../src/index.js'; +import { checkTestAndTagName } from '../helpers.js'; describe('isNameRequired', () => { const tests: [string, { given: Parameters[0]; want: ReturnType }][] = [ diff --git a/test/utils.test.ts b/test/node/utils.test.ts similarity index 96% rename from test/utils.test.ts rename to test/node/utils.test.ts index 831c4d1..8b20efd 100644 --- a/test/utils.test.ts +++ b/test/node/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { removeProhibited } from '../src/index.js'; +import { removeProhibited } from '../../src/index.js'; describe('removeProhibited', () => { test('default', () => { diff --git a/tsconfig.json b/tsconfig.json index 69d485c..ce0dc49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,14 @@ "module": "NodeNext", "moduleResolution": "nodenext", "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "outDir": "dist", "sourceMap": true, "strict": true, "target": "ESNext", - "noUncheckedIndexedAccess": true + "types": ["@vitest/browser/providers/playwright"] }, "include": ["src"], "exclude": ["dist", "node_modules"] diff --git a/vitest.config.ts b/vitest.config.ts index 647a9e5..de87991 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,5 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - environment: 'jsdom', - }, + test: {}, }); diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000..088ee50 --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,36 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace([ + { + test: { + name: 'node', + include: ['./test/node/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'jsdom', + include: ['./test/dom/**/*.test.ts'], + environment: 'jsdom', + }, + }, + { + test: { + name: 'happy-dom', + include: ['./test/dom/**/*.test.ts'], + environment: 'happy-dom', + }, + }, + { + test: { + name: 'browser', + include: ['./test/dom/**/*.test.ts'], + browser: { + provider: 'playwright', + enabled: true, + instances: [{ browser: 'chromium' }], + }, + }, + }, +]); From a38d8e89c27ff2777193af00161716f951684b55 Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Tue, 28 Jan 2025 09:28:01 -0800 Subject: [PATCH 2/4] Update README.md Co-authored-by: Craig Morten <124147726+jlp-craigmorten@users.noreply.github.com> --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 8b8fd5e..f98fafd 100644 --- a/README.md +++ b/README.md @@ -376,9 +376,7 @@ getRole({ tagName: "li" }, { ancestors: [] }); // "generic" These are all the elements that have assumed context (i.e. different behavior in Node.js): `
                `, `
              • `, ``, `
              • `, `
                `, `
                with ancestors ['table'] will be role 'cell' - * - with ancestors ['grid'] or ['treegrid'] will be 'gridcell' - * - with NO ancestors ([]) will be no role (`undefined`) + * ```html + * + * + * + * + * + * + *
                + * ``` + * Could be represented as: * - * This list does NOT have to be complete; e.g. `'row'` can be skipped as it - * doesn’t affect behavior. But irrelevant parents may be supplied for ease of - * use, and only the first significant ancestor will apply. + * ```ts + * getRole( + * { tagName: 'td' }, + * { ancestors: [{ tagName: 'tr' }, { tagName: 'tbody' }, { tagName: 'table' }] }, + * ); + * ``` + * + * Note: This list does _not_ have to be complete; simply listing out the significant elements that + * affect a11y. + * @see https://github.com/drwpow/html-aria/tree/dom-support#nodejs-vs-dom-behavior */ ancestors?: VirtualAncestorList; /** Ignore role attribute in calculation to get the intrinsic role. Needed in some fallback scenarios. */ @@ -32,7 +47,7 @@ export interface GetRoleOptions { /** * Get the corresponding ARIA role for a given HTML element. * `undefined` means “no corresponding role”. - * Note this does NOT traverse the DOM, because we assume it’s not fully available, e.g. in Node.js, React Components, lint rules, etc. + * This traverses the DOM when available. * @see https://www.w3.org/TR/html-aria/ */ export function getRole(element: Element | VirtualElement, options?: GetRoleOptions): RoleData | undefined { From 9cec04820b25c74b31ccf20f89691cccfa81bfb5 Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Tue, 28 Jan 2025 12:59:27 -0800 Subject: [PATCH 4/4] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f98fafd..2378441 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ Utilities for creating accessible HTML based on the [latest ARIA 1.3 specs](http This is designed to be a better replacement for aria-query when working with HTML. The reasons are: -- aria-query neglects the critical specs [HTML Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/) and [HTML to ARIA](https://www.w3.org/TR/html-aria). With just the ARIA spec alone, it’s insufficient for working with HTML. +- html-aria is designed to reduce mistakes, while aria-query’s APIs are easy to “hold it wrong.” The information may not be _incorrect_, but often are locked behind several successful operations you must know to connect to get the right result. +- html-aria and aria-query both follow the [ARIA 1.3 spec](https://w3c.github.io/aria/), but that’s only one part. There are also the [HTML Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/) and [HTML to ARIA](https://www.w3.org/TR/html-aria) specs that are critical to working with HTML. While aria-query follows these other documents when it can, its design makes it difficult to apply the advice from all specs, often producing incomplete or incorrect results. - html-aria supports ARIA 1.3 while aria-query is still on ARIA 1.2 -html-aria is also designed to be easier-to-use to prevent mistakes, smaller, is ESM tree-shakeable, and more performant (~100× faster than aria-query). +html-aria is also ESM-compatible and [more performant](./test/node/html-aria.bench.ts). ## Usage