Skip to content

Commit

Permalink
fix: implement radio group logic (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
jlp-craigmorten authored Apr 13, 2024
1 parent 3c1690a commit 632f687
Show file tree
Hide file tree
Showing 4 changed files with 317 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AccessibilityNodeTree } from "../../../createAccessibilityTree";
import { isElement } from "../../../isElement";

const getFirstNestedChildrenByRole = ({
role,
Expand All @@ -15,22 +16,104 @@ const getFirstNestedChildrenByRole = ({
return getFirstNestedChildrenByRole({ role, tree: child });
});

const getSiblingsByRoleAndLevel = ({
const getParentByRole = ({
role,
tree,
}: {
role: string;
tree: AccessibilityNodeTree;
}): AccessibilityNodeTree[] => {
}): AccessibilityNodeTree => {
let parentTree = tree;

while (parentTree.role !== role && parentTree.parentAccessibilityNodeTree) {
parentTree = parentTree.parentAccessibilityNodeTree;
}

return parentTree;
};

const getSiblingsByRoleAndLevel = ({
role,
parentRole = role,
tree,
}: {
role: string;
parentRole?: string;
tree: AccessibilityNodeTree;
}): AccessibilityNodeTree[] => {
const parentTree = getParentByRole({ role: parentRole, tree });

return getFirstNestedChildrenByRole({ role, tree: parentTree });
};

const getFormOwnerTree = ({ tree }: { tree: AccessibilityNodeTree }) =>
getParentByRole({ role: "form", tree });

const getRadioInputsByName = ({
name,
tree,
}: {
name: string;
tree: AccessibilityNodeTree;
}): AccessibilityNodeTree[] =>
tree.children.flatMap((child) => {
if (isElement(child.node) && child.node.getAttribute("name") === name) {
return child;
}

return getRadioInputsByName({ name, tree: child });
});

/**
* The radio button group that contains an input element a also contains all
* the other input elements b that fulfill all of the following conditions:
*
* - The input element b's type attribute is in the Radio Button state.
* - Either a and b have the same form owner, or they both have no form owner.
* - Both a and b are in the same tree.
* - They both have a name attribute, their name attributes are not empty, and
* the value of a's name attribute equals the value of b's name attribute.
*
* REF: https://html.spec.whatwg.org/multipage/input.html#radio-button-group
*/
const getRadioGroup = ({
node,
tree,
}: {
node: HTMLElement;
tree: AccessibilityNodeTree;
}) => {
/**
* Authors SHOULD ensure that elements with role radio are explicitly grouped
* in order to indicate which ones affect the same value. This is achieved by
* enclosing the radio elements in an element with role radiogroup. If it is
* not possible to make the radio buttons DOM children of the radiogroup,
* authors SHOULD use the aria-owns attribute on the radiogroup element to
* indicate the relationship to its children.
*/
if (node.localName !== "input") {
return getSiblingsByRoleAndLevel({
role: "radio",
parentRole: "radiogroup",
tree,
});
}

if (!node.hasAttribute("name")) {
return [];
}

const name = node.getAttribute("name")!;

if (!name) {
return [];
}

const formOwnerTree = getFormOwnerTree({ tree });

return getRadioInputsByName({ name, tree: formOwnerTree });
};

const getChildrenByRole = ({
role,
tree,
Expand Down Expand Up @@ -75,16 +158,29 @@ const getChildrenByRole = ({
* REF: https://www.w3.org/TR/core-aam-1.2/#mapping_additional_position
*/
export const getSet = ({
node,
role,
tree,
}: {
role: string;
node: HTMLElement;
tree: AccessibilityNodeTree;
}): AccessibilityNodeTree[] => {
role: string;
}): Pick<AccessibilityNodeTree, "node">[] => {
if (role === "treeitem") {
return getSiblingsByRoleAndLevel({ role, tree });
}

/**
* With aria-setsize value reflecting number of type=radio input elements
* within the radio button group and aria-posinset value reflecting the
* elements position within the radio button group.
*
* REF: https://www.w3.org/TR/html-aam-1.0/#el-input-radio
*/
if (role === "radio") {
return getRadioGroup({ node, tree });
}

return getChildrenByRole({
role,
tree,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,55 @@ const headingLocalNameToLevelMap: Record<string, string> = {
h6: "6",
};

const getNodeSet = ({
node,
role,
tree,
}: {
node: HTMLElement;
tree: AccessibilityNodeTree | null;
role: string;
}): Pick<AccessibilityNodeTree, "node">[] | null => {
if (!tree) {
return null;
}

/**
* When an article is in the context of a feed, the author MAY specify
* values for aria-posinset and aria-setsize.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#article
*
* This is interpreted as the author being allowed to specify a value when
* nested in a feed, but there are no requirements in the specifications
* for an article role to expose an implicit value, even within a feed.
*/
if (role === "article") {
return null;
}

/**
* While the row role can be used in a table, grid, or treegrid, the semantics
* of aria-expanded, aria-posinset, aria-setsize, and aria-level are only
* applicable to the hierarchical structure of an interactive tree grid.
* Therefore, authors MUST NOT apply aria-expanded, aria-posinset,
* aria-setsize, and aria-level to a row that descends from a table or grid,
* and user agents SHOULD NOT expose any of these four properties to assistive
* technologies unless the row descends from a treegrid.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#row
*/
if (role === "row" && !hasTreegridAncestor(tree)) {
return null;
}

return getSet({
node,
role,
tree,
});
};

type Mapper = ({
node,
tree,
Expand Down Expand Up @@ -92,43 +141,12 @@ const mapHtmlElementAriaToImplicitValue: Record<string, Mapper> = {
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-posinset
*/
"aria-posinset": ({ node, tree, role }) => {
if (!tree) {
return "";
}
const nodeSet = getNodeSet({ node, role, tree });

/**
* When an article is in the context of a feed, the author MAY specify
* values for aria-posinset and aria-setsize.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#article
*
* This is interpreted as the author being allowed to specify a value when
* nested in a feed, but there are no requirements in the specifications
* for an article role to expose an implicit value, even within a feed.
*/
if (role === "article") {
if (!nodeSet?.length) {
return "";
}

/**
* While the row role can be used in a table, grid, or treegrid, the semantics
* of aria-expanded, aria-posinset, aria-setsize, and aria-level are only
* applicable to the hierarchical structure of an interactive tree grid.
* Therefore, authors MUST NOT apply aria-expanded, aria-posinset,
* aria-setsize, and aria-level to a row that descends from a table or grid,
* and user agents SHOULD NOT expose any of these four properties to assistive
* technologies unless the row descends from a treegrid.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#row
*/
if (role === "row" && !hasTreegridAncestor(tree)) {
return "";
}

const nodeSet = getSet({
role,
tree,
});
const index = nodeSet.findIndex((child) => child.node === node);

return `${index + 1}`;
Expand All @@ -152,45 +170,13 @@ const mapHtmlElementAriaToImplicitValue: Record<string, Mapper> = {
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-setsize
*/
"aria-setsize": ({ tree, role }) => {
if (!tree) {
return "";
}
"aria-setsize": ({ node, tree, role }) => {
const nodeSet = getNodeSet({ node, role, tree });

/**
* When an article is in the context of a feed, the author MAY specify
* values for aria-posinset and aria-setsize.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#article
*
* This is interpreted as the author being allowed to specify a value when
* nested in a feed, but there are no requirements in the specifications
* for an article role to expose an implicit value, even within a feed.
*/
if (role === "article") {
if (!nodeSet?.length) {
return "";
}

/**
* While the row role can be used in a table, grid, or treegrid, the semantics
* of aria-expanded, aria-posinset, aria-setsize, and aria-level are only
* applicable to the hierarchical structure of an interactive tree grid.
* Therefore, authors MUST NOT apply aria-expanded, aria-posinset,
* aria-setsize, and aria-level to a row that descends from a table or grid,
* and user agents SHOULD NOT expose any of these four properties to assistive
* technologies unless the row descends from a treegrid.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#row
*/
if (role === "row" && !hasTreegridAncestor(tree)) {
return "";
}

const nodeSet = getSet({
role,
tree,
});

return `${nodeSet.length}`;
},
};
Expand Down
8 changes: 2 additions & 6 deletions test/int/accessibleValue.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ describe("Placeholder Attribute Property", () => {
await virtual.next();
await virtual.next();

expect(await virtual.lastSpokenPhrase()).toBe(
"radio, Label, not checked, position 1, set size 1"
);
expect(await virtual.lastSpokenPhrase()).toBe("radio, Label, not checked");

await virtual.stop();
});
Expand All @@ -95,9 +93,7 @@ describe("Placeholder Attribute Property", () => {
await virtual.next();
await virtual.next();

expect(await virtual.lastSpokenPhrase()).toBe(
"radio, Label, not checked, position 1, set size 1"
);
expect(await virtual.lastSpokenPhrase()).toBe("radio, Label, not checked");

await virtual.stop();
});
Expand Down
Loading

0 comments on commit 632f687

Please sign in to comment.