Skip to content

Commit 172d3dd

Browse files
Copilotcsharpfritz
andauthored
Add opt-in accessibility features to TreeView component (#323)
* Initial plan * Add UseAccessibilityFeatures parameter with ARIA attributes and tabindex support Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> * Update TreeView documentation with accessibility features Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> * Add accessibility sample page for TreeView Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> * Address code review feedback - remove redundant await statements Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
1 parent 9fc6dd9 commit 172d3dd

7 files changed

Lines changed: 424 additions & 3 deletions

File tree

docs/NavigationControls/TreeView.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,71 @@ The TreeView component is meant to emulate the asp:TreeView control in markup an
99
- XmlDocument databinding
1010
- SiteMap databinding as an XmlDocument
1111
- Databinding events
12+
- **Accessibility features** (see below)
13+
14+
## Accessibility Features
15+
16+
The TreeView component includes an optional `UseAccessibilityFeatures` parameter that enhances the component for users with disabilities, particularly for keyboard navigation and screen reader support.
17+
18+
### Enabling Accessibility Features
19+
20+
To enable accessibility features, set the `UseAccessibilityFeatures` parameter to `true`:
21+
22+
```html
23+
<TreeView UseAccessibilityFeatures="true">
24+
<Nodes>
25+
<TreeNode Text="Root Node" Expanded="true">
26+
<TreeNode Text="Child Node" />
27+
</TreeNode>
28+
</Nodes>
29+
</TreeView>
30+
```
31+
32+
### When Accessibility Features are Enabled
33+
34+
The TreeView component provides the following enhancements:
35+
36+
#### ARIA Attributes
37+
38+
- **`role="tree"`**: Applied to the TreeView container to identify it as a tree widget
39+
- **`aria-label="Tree navigation"`**: Provides a descriptive label for screen readers
40+
- **`role="treeitem"`**: Applied to each TreeNode to identify it as a tree item
41+
- **`aria-level`**: Indicates the depth level of each node (1 for root nodes, 2 for children, etc.)
42+
- **`aria-expanded`**: For nodes with children, indicates whether the node is expanded (`"true"`) or collapsed (`"false"`)
43+
- **`aria-label`**: Added to expand/collapse links to provide descriptive text for screen readers
44+
45+
#### Keyboard Navigation
46+
47+
With accessibility features enabled, users can navigate the TreeView using keyboard shortcuts:
48+
49+
- **Arrow Right (→)**: Expands a collapsed node that has children
50+
- **Arrow Left (←)**: Collapses an expanded node that has children
51+
- **Enter or Space**: Activates the node's link or expands/collapses the node
52+
- **Tab**: Moves focus into and out of the TreeView
53+
54+
#### Focus Management
55+
56+
- Each TreeNode receives a `tabindex="0"` attribute, making it keyboard accessible
57+
- Nodes can receive focus and respond to keyboard events
58+
- Visual focus indicators follow browser defaults
59+
60+
### Benefits for Different Users
61+
62+
The accessibility features address several user needs:
63+
64+
1. **Keyboard Users**: Can navigate and interact with the TreeView without a mouse
65+
2. **Screen Reader Users**: Get proper semantic information about the tree structure and node states
66+
3. **Speech Recognition Users**: Benefit from larger interactive areas due to keyboard focus support
67+
4. **Touch Device Users**: While the core issue of small touch targets isn't fully addressed, keyboard equivalents provide an alternative interaction method
68+
69+
### Backwards Compatibility
70+
71+
By default, `UseAccessibilityFeatures` is `false`, ensuring existing implementations continue to work without changes. The HTML output remains identical to the original Web Forms TreeView when accessibility features are disabled.
1272

1373
## Usage Notes
1474

1575
- ShowCheckBoxes attribute, when specifying multiple values, should be separated by a vertical pipe `|` instead of commas
76+
- For the best user experience with assistive technologies, set `UseAccessibilityFeatures="true"`
1677

1778
## Web Forms Declarative Syntax
1879

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
@page "/ControlSamples/TreeView/Accessibility"
2+
3+
<h2>TreeView with Accessibility Features</h2>
4+
5+
<p>This sample demonstrates the accessibility enhancements available in the TreeView component.</p>
6+
7+
<h3>Standard TreeView (without accessibility features)</h3>
8+
<p>This is the default TreeView behavior, which matches the original Web Forms control.</p>
9+
10+
<TreeView ID="StandardTreeView" runat="server">
11+
<Nodes>
12+
<TreeNode Value="Home"
13+
NavigateUrl="/"
14+
Text="Home"
15+
Expanded="true">
16+
<TreeNode Value="Products" Text="Products" Expanded="true">
17+
<TreeNode Value="Electronics" Text="Electronics">
18+
<TreeNode Value="Computers" Text="Computers" />
19+
<TreeNode Value="Phones" Text="Phones" />
20+
</TreeNode>
21+
<TreeNode Value="Books" Text="Books" />
22+
</TreeNode>
23+
<TreeNode Value="About" Text="About Us" NavigateUrl="/about" />
24+
</TreeNode>
25+
</Nodes>
26+
</TreeView>
27+
28+
<hr />
29+
30+
<h3>TreeView with Accessibility Features Enabled</h3>
31+
<p>
32+
Set <code>UseAccessibilityFeatures="true"</code> to enable:
33+
</p>
34+
<ul>
35+
<li><strong>ARIA attributes</strong> for screen readers (role="tree", aria-expanded, aria-level, etc.)</li>
36+
<li><strong>Keyboard navigation</strong>:
37+
<ul>
38+
<li><kbd>Arrow Right</kbd> - Expand a collapsed node</li>
39+
<li><kbd>Arrow Left</kbd> - Collapse an expanded node</li>
40+
<li><kbd>Enter</kbd> or <kbd>Space</kbd> - Activate node link or toggle expansion</li>
41+
<li><kbd>Tab</kbd> - Move focus between nodes</li>
42+
</ul>
43+
</li>
44+
<li><strong>Focus management</strong> with tabindex for keyboard users</li>
45+
</ul>
46+
47+
<TreeView ID="AccessibleTreeView" runat="server" UseAccessibilityFeatures="true">
48+
<Nodes>
49+
<TreeNode Value="Home"
50+
NavigateUrl="/"
51+
Text="Home"
52+
Expanded="true">
53+
<TreeNode Value="Products" Text="Products" Expanded="true">
54+
<TreeNode Value="Electronics" Text="Electronics">
55+
<TreeNode Value="Computers" Text="Computers" />
56+
<TreeNode Value="Phones" Text="Phones" />
57+
</TreeNode>
58+
<TreeNode Value="Books" Text="Books" />
59+
</TreeNode>
60+
<TreeNode Value="About" Text="About Us" NavigateUrl="/about" />
61+
</TreeNode>
62+
</Nodes>
63+
</TreeView>
64+
65+
<hr />
66+
67+
<h3>Accessibility Benefits</h3>
68+
<p>The <code>UseAccessibilityFeatures</code> parameter provides benefits for:</p>
69+
70+
<div style="margin: 20px 0;">
71+
<h4>🎹 Keyboard Users</h4>
72+
<p>Navigate and interact with the TreeView entirely using keyboard shortcuts, without requiring a mouse.</p>
73+
</div>
74+
75+
<div style="margin: 20px 0;">
76+
<h4>🔊 Screen Reader Users</h4>
77+
<p>Get proper semantic information about tree structure, node states (expanded/collapsed), and depth levels through ARIA attributes.</p>
78+
</div>
79+
80+
<div style="margin: 20px 0;">
81+
<h4>🎤 Speech Recognition Users</h4>
82+
<p>Benefit from larger interactive areas and keyboard equivalents, making voice commands more reliable.</p>
83+
</div>
84+
85+
<div style="margin: 20px 0;">
86+
<h4>👆 Touch Device Users</h4>
87+
<p>Use keyboard equivalents as an alternative to small touch targets for expand/collapse actions.</p>
88+
</div>
89+
90+
<hr />
91+
92+
<h3>Code Example</h3>
93+
<code>
94+
&lt;TreeView UseAccessibilityFeatures="true"&gt;<br />
95+
&nbsp;&nbsp;&lt;Nodes&gt;<br />
96+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;TreeNode Text="Home" Expanded="true"&gt;<br />
97+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;TreeNode Text="Products"&gt;...&lt;/TreeNode&gt;<br />
98+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;TreeNode Text="About"&gt;...&lt;/TreeNode&gt;<br />
99+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;/TreeNode&gt;<br />
100+
&nbsp;&nbsp;&lt;/Nodes&gt;<br />
101+
&lt;/TreeView&gt;
102+
</code>
103+
104+
@code {
105+
// No code-behind needed for this demo
106+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
2+
@code {
3+
4+
[Fact]
5+
public void TreeView_UseAccessibilityFeatures_AddsRoleAttribute()
6+
{
7+
// Arrange/Act
8+
var cut = Render(
9+
@<TreeView UseAccessibilityFeatures="true">
10+
<Nodes>
11+
<TreeNode Text="Root Node" Expanded="true">
12+
<TreeNode Text="Child Node" />
13+
</TreeNode>
14+
</Nodes>
15+
</TreeView>
16+
);
17+
18+
// Assert
19+
var treeViewDiv = cut.Find("div");
20+
treeViewDiv.GetAttribute("role").ShouldBe("tree");
21+
}
22+
23+
[Fact]
24+
public void TreeView_UseAccessibilityFeatures_AddsAriaLabel()
25+
{
26+
// Arrange/Act
27+
var cut = Render(
28+
@<TreeView UseAccessibilityFeatures="true">
29+
<Nodes>
30+
<TreeNode Text="Root Node" />
31+
</Nodes>
32+
</TreeView>
33+
);
34+
35+
// Assert
36+
var treeViewDiv = cut.Find("div");
37+
treeViewDiv.GetAttribute("aria-label").ShouldBe("Tree navigation");
38+
}
39+
40+
[Fact]
41+
public void TreeView_WithoutAccessibilityFeatures_NoRoleAttribute()
42+
{
43+
// Arrange/Act
44+
var cut = Render(
45+
@<TreeView UseAccessibilityFeatures="false">
46+
<Nodes>
47+
<TreeNode Text="Root Node" />
48+
</Nodes>
49+
</TreeView>
50+
);
51+
52+
// Assert
53+
var treeViewDiv = cut.Find("div");
54+
treeViewDiv.HasAttribute("role").ShouldBeFalse();
55+
}
56+
57+
[Fact]
58+
public void TreeNode_UseAccessibilityFeatures_AddsTreeItemRole()
59+
{
60+
// Arrange/Act
61+
var cut = Render(
62+
@<TreeView UseAccessibilityFeatures="true">
63+
<Nodes>
64+
<TreeNode Text="Root Node" Expanded="true">
65+
<TreeNode Text="Child Node" />
66+
</TreeNode>
67+
</Nodes>
68+
</TreeView>
69+
);
70+
71+
// Assert
72+
var tables = cut.FindAll("table");
73+
tables.Count().ShouldBeGreaterThan(0);
74+
75+
foreach (var table in tables)
76+
{
77+
table.GetAttribute("role").ShouldBe("treeitem");
78+
}
79+
}
80+
81+
[Fact]
82+
public void TreeNode_UseAccessibilityFeatures_AddsAriaLevel()
83+
{
84+
// Arrange/Act
85+
var cut = Render(
86+
@<TreeView UseAccessibilityFeatures="true">
87+
<Nodes>
88+
<TreeNode Text="Root Node" Expanded="true">
89+
<TreeNode Text="Child Node" Expanded="true">
90+
<TreeNode Text="Grandchild Node" />
91+
</TreeNode>
92+
</TreeNode>
93+
</Nodes>
94+
</TreeView>
95+
);
96+
97+
// Assert
98+
var tables = cut.FindAll("table");
99+
tables.Count().ShouldBe(3);
100+
101+
// Root node should have aria-level="1"
102+
tables[0].GetAttribute("aria-level").ShouldBe("1");
103+
104+
// Child node should have aria-level="2"
105+
tables[1].GetAttribute("aria-level").ShouldBe("2");
106+
107+
// Grandchild node should have aria-level="3"
108+
tables[2].GetAttribute("aria-level").ShouldBe("3");
109+
}
110+
111+
[Fact]
112+
public void TreeNode_UseAccessibilityFeatures_AddsAriaExpandedToParentNodes()
113+
{
114+
// Arrange/Act
115+
var cut = Render(
116+
@<TreeView UseAccessibilityFeatures="true">
117+
<Nodes>
118+
<TreeNode Text="Root Node" Expanded="true">
119+
<TreeNode Text="Child Node" />
120+
</TreeNode>
121+
</Nodes>
122+
</TreeView>
123+
);
124+
125+
// Assert
126+
var tables = cut.FindAll("table");
127+
128+
// Root node (has children, expanded)
129+
tables[0].GetAttribute("aria-expanded").ShouldBe("true");
130+
131+
// Child node (no children, should not have aria-expanded)
132+
tables[1].HasAttribute("aria-expanded").ShouldBeFalse();
133+
}
134+
135+
[Fact]
136+
public void TreeNode_UseAccessibilityFeatures_AriaExpandedReflectsCollapsedState()
137+
{
138+
// Arrange/Act
139+
var cut = Render(
140+
@<TreeView UseAccessibilityFeatures="true">
141+
<Nodes>
142+
<TreeNode Text="Root Node" Expanded="false">
143+
<TreeNode Text="Child Node" />
144+
</TreeNode>
145+
</Nodes>
146+
</TreeView>
147+
);
148+
149+
// Assert
150+
var tables = cut.FindAll("table");
151+
152+
// Root node (has children, collapsed)
153+
tables[0].GetAttribute("aria-expanded").ShouldBe("false");
154+
}
155+
156+
[Fact]
157+
public void TreeNode_UseAccessibilityFeatures_AddsTabindex()
158+
{
159+
// Arrange/Act
160+
var cut = Render(
161+
@<TreeView UseAccessibilityFeatures="true">
162+
<Nodes>
163+
<TreeNode Text="Root Node" />
164+
</Nodes>
165+
</TreeView>
166+
);
167+
168+
// Assert
169+
var table = cut.Find("table");
170+
table.GetAttribute("tabindex").ShouldBe("0");
171+
}
172+
173+
[Fact]
174+
public void TreeNode_WithoutAccessibilityFeatures_NoTabindex()
175+
{
176+
// Arrange/Act
177+
var cut = Render(
178+
@<TreeView UseAccessibilityFeatures="false">
179+
<Nodes>
180+
<TreeNode Text="Root Node" />
181+
</Nodes>
182+
</TreeView>
183+
);
184+
185+
// Assert
186+
var table = cut.Find("table");
187+
table.HasAttribute("tabindex").ShouldBeFalse();
188+
}
189+
190+
[Fact]
191+
public void TreeNode_UseAccessibilityFeatures_AddsAriaLabelToExpandLink()
192+
{
193+
// Arrange/Act
194+
var cut = Render(
195+
@<TreeView UseAccessibilityFeatures="true" ShowExpandCollapse="true">
196+
<Nodes>
197+
<TreeNode Text="Root Node" Expanded="true">
198+
<TreeNode Text="Child Node" />
199+
</TreeNode>
200+
</Nodes>
201+
</TreeView>
202+
);
203+
204+
// Assert
205+
var expandLinks = cut.FindAll("a").Where(a => a.GetElementsByTagName("img").Any());
206+
expandLinks.Count().ShouldBeGreaterThan(0);
207+
208+
var firstExpandLink = expandLinks.First();
209+
firstExpandLink.GetAttribute("aria-label").ShouldBe("Collapse Root Node");
210+
}
211+
212+
}

0 commit comments

Comments
 (0)