mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-08-10 08:37:17 +01:00
Announce resource tree selection state to screen readers (#4798482)
The Data Explorer left-nav resource tree did not expose selection state to assistive technology. Screen readers announced tree items (e.g. "Documents, 1 of 2, level 3") without conveying whether the item was selected, failing MAS 4.1.2 (Name, Role, Value). Selection in the tree is a custom app-state concept (node.isSelected / shouldShowAsSelected) and does not use Fluent v9's built-in selectionMode. Wire the existing selection state to the aria-selected prop on TreeItem so selectable nodes announce "selected"/"not selected". Pure grouping nodes (no isSelected) omit the attribute, matching the ARIA single-select tree pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 956998dd-4849-4f7d-a027-b7190018e089
This commit is contained in:
@@ -110,6 +110,42 @@ describe("TreeNodeComponent", () => {
|
|||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets aria-selected=true on a selected selectable node", () => {
|
||||||
|
const node = generateTestNode("root", {
|
||||||
|
isSelected: () => true,
|
||||||
|
});
|
||||||
|
const component = shallow(<TreeNodeComponent openItems={[]} node={node} treeNodeId={node.id} />);
|
||||||
|
expect(component.find(TreeItem).first().props()["aria-selected"]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets aria-selected=false on an unselected selectable node", () => {
|
||||||
|
const node = generateTestNode("root", {
|
||||||
|
isSelected: () => false,
|
||||||
|
});
|
||||||
|
const component = shallow(<TreeNodeComponent openItems={[]} node={node} treeNodeId={node.id} />);
|
||||||
|
expect(component.find(TreeItem).first().props()["aria-selected"]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set aria-selected on a non-selectable (grouping) node", () => {
|
||||||
|
const node = generateTestNode("root");
|
||||||
|
delete node.isSelected;
|
||||||
|
const component = shallow(<TreeNodeComponent openItems={[]} node={node} treeNodeId={node.id} />);
|
||||||
|
expect(component.find(TreeItem).first().props()["aria-selected"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets aria-selected=false on a selected parent when a descendant is selected", () => {
|
||||||
|
const node = generateTestNode("root", {
|
||||||
|
isSelected: () => true,
|
||||||
|
children: [
|
||||||
|
generateTestNode("child1", {
|
||||||
|
isSelected: () => true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const component = shallow(<TreeNodeComponent openItems={[]} node={node} treeNodeId={node.id} />);
|
||||||
|
expect(component.find(TreeItem).first().props()["aria-selected"]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders an icon if the node has one", () => {
|
it("renders an icon if the node has one", () => {
|
||||||
const node = generateTestNode("root", {
|
const node = generateTestNode("root", {
|
||||||
iconSrc: "the-icon.svg",
|
iconSrc: "the-icon.svg",
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
itemType={isBranch ? "branch" : "leaf"}
|
itemType={isBranch ? "branch" : "leaf"}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
className={treeStyles.treeItem}
|
className={treeStyles.treeItem}
|
||||||
|
// Expose selection state to screen readers for selectable nodes so they announce "selected"/"not selected".
|
||||||
|
// Pure grouping nodes (no isSelected) omit the attribute, matching the ARIA single-select tree pattern.
|
||||||
|
aria-selected={node.isSelected ? shouldShowAsSelected : undefined}
|
||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
treeNodeId="root"
|
treeNodeId="root"
|
||||||
>
|
>
|
||||||
<TreeItem
|
<TreeItem
|
||||||
|
aria-selected={false}
|
||||||
className=""
|
className=""
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
itemType="branch"
|
itemType="branch"
|
||||||
@@ -120,6 +121,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
aria-expanded={false}
|
||||||
aria-level={0}
|
aria-level={0}
|
||||||
|
aria-selected={false}
|
||||||
className="fui-TreeItem r15xhw3a"
|
className="fui-TreeItem r15xhw3a"
|
||||||
data-fui-tree-item-value="root"
|
data-fui-tree-item-value="root"
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
@@ -218,6 +220,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"current": <div
|
"current": <div
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-level="0"
|
aria-level="0"
|
||||||
|
aria-selected="false"
|
||||||
class="fui-TreeItem r15xhw3a"
|
class="fui-TreeItem r15xhw3a"
|
||||||
data-fui-tree-item-value="root"
|
data-fui-tree-item-value="root"
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
@@ -1595,6 +1598,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
|
|
||||||
exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = `
|
exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = `
|
||||||
<TreeItem
|
<TreeItem
|
||||||
|
aria-selected={true}
|
||||||
className=""
|
className=""
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
itemType="branch"
|
itemType="branch"
|
||||||
@@ -1678,6 +1682,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
|||||||
|
|
||||||
exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = `
|
exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = `
|
||||||
<TreeItem
|
<TreeItem
|
||||||
|
aria-selected={false}
|
||||||
className=""
|
className=""
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
itemType="branch"
|
itemType="branch"
|
||||||
@@ -1762,6 +1767,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
|||||||
|
|
||||||
exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
||||||
<TreeItem
|
<TreeItem
|
||||||
|
aria-selected={true}
|
||||||
className=""
|
className=""
|
||||||
data-test="TreeNodeContainer:root"
|
data-test="TreeNodeContainer:root"
|
||||||
itemType="leaf"
|
itemType="leaf"
|
||||||
|
|||||||
Reference in New Issue
Block a user