Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: implement radio group logic #73

Merged
merged 2 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading