From 05d95cb467bbc9cbd4121f1ed27c532493871c26 Mon Sep 17 00:00:00 2001 From: Jade Welton Date: Mon, 20 Jul 2026 15:21:49 -0700 Subject: [PATCH] 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 --- .../TreeComponent/TreeNodeComponent.test.tsx | 36 +++++++++++++++++++ .../TreeComponent/TreeNodeComponent.tsx | 3 ++ .../TreeNodeComponent.test.tsx.snap | 6 ++++ 3 files changed, 45 insertions(+) diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx index 2b12de615..3ee538262 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx @@ -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(); + 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(); + 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(); + 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(); + 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", diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx index 74a717b68..d4312bd14 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -172,6 +172,9 @@ export const TreeNodeComponent: React.FC = ({ 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} >