mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-08-10 16:47:13 +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();
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const node = generateTestNode("root", {
|
||||
iconSrc: "the-icon.svg",
|
||||
|
||||
@@ -172,6 +172,9 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
itemType={isBranch ? "branch" : "leaf"}
|
||||
onOpenChange={onOpenChange}
|
||||
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
|
||||
className={mergeClasses(
|
||||
|
||||
@@ -111,6 +111,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
treeNodeId="root"
|
||||
>
|
||||
<TreeItem
|
||||
aria-selected={false}
|
||||
className=""
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="branch"
|
||||
@@ -120,6 +121,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-level={0}
|
||||
aria-selected={false}
|
||||
className="fui-TreeItem r15xhw3a"
|
||||
data-fui-tree-item-value="root"
|
||||
data-test="TreeNodeContainer:root"
|
||||
@@ -218,6 +220,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
||||
"current": <div
|
||||
aria-expanded="false"
|
||||
aria-level="0"
|
||||
aria-selected="false"
|
||||
class="fui-TreeItem r15xhw3a"
|
||||
data-fui-tree-item-value="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`] = `
|
||||
<TreeItem
|
||||
aria-selected={true}
|
||||
className=""
|
||||
data-test="TreeNodeContainer:root"
|
||||
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`] = `
|
||||
<TreeItem
|
||||
aria-selected={false}
|
||||
className=""
|
||||
data-test="TreeNodeContainer:root"
|
||||
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`] = `
|
||||
<TreeItem
|
||||
aria-selected={true}
|
||||
className=""
|
||||
data-test="TreeNodeContainer:root"
|
||||
itemType="leaf"
|
||||
|
||||
Reference in New Issue
Block a user