Skip to content

Commit e55d7d7

Browse files
authored
Fix screen reader issues with CardAction/Button/MenuItem/NavigationViewItem (#1235)
* Add narration support to CardAction * Fix screen readers trying to read IconElement in some cases * Add narration support to NavigationViewItem * Button workaround appears to be unnecessary now
1 parent 89fda9c commit e55d7d7

File tree

5 files changed

+205
-1
lines changed

5 files changed

+205
-1
lines changed

src/Wpf.Ui/Controls/CardAction/CardAction.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// All Rights Reserved.
55

66
// ReSharper disable once CheckNamespace
7+
using System.Windows.Automation.Peers;
8+
79
namespace Wpf.Ui.Controls;
810

911
/// <summary>
@@ -48,4 +50,9 @@ public IconElement? Icon
4850
get => (IconElement?)GetValue(IconProperty);
4951
set => SetValue(IconProperty, value);
5052
}
51-
}
53+
54+
protected override AutomationPeer OnCreateAutomationPeer()
55+
{
56+
return new CardActionAutomationPeer(this);
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This Source Code Form is subject to the terms of the MIT License.
2+
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
3+
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
4+
// All Rights Reserved.
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
using System.Windows.Automation;
12+
using System.Windows.Automation.Peers;
13+
14+
namespace Wpf.Ui.Controls;
15+
16+
internal class CardActionAutomationPeer : FrameworkElementAutomationPeer
17+
{
18+
private readonly CardAction _owner;
19+
20+
public CardActionAutomationPeer(CardAction owner)
21+
: base(owner)
22+
{
23+
_owner = owner;
24+
}
25+
26+
protected override string GetClassNameCore()
27+
{
28+
return "Button";
29+
}
30+
31+
protected override AutomationControlType GetAutomationControlTypeCore()
32+
{
33+
return AutomationControlType.Button;
34+
}
35+
36+
public override object GetPattern(PatternInterface patternInterface)
37+
{
38+
if (patternInterface == PatternInterface.ItemContainer)
39+
{
40+
return this;
41+
}
42+
43+
return base.GetPattern(patternInterface);
44+
}
45+
46+
protected override AutomationPeer GetLabeledByCore()
47+
{
48+
if (_owner.Content is UIElement element)
49+
{
50+
return CreatePeerForElement(element);
51+
}
52+
53+
return base.GetLabeledByCore();
54+
}
55+
56+
protected override string GetNameCore()
57+
{
58+
var result = base.GetNameCore() ?? string.Empty;
59+
60+
if (result == string.Empty)
61+
{
62+
result = AutomationProperties.GetName(_owner);
63+
}
64+
65+
if (result == string.Empty && _owner.Content is DependencyObject d)
66+
{
67+
result = AutomationProperties.GetName(d);
68+
}
69+
70+
if (result == string.Empty && _owner.Content is string s)
71+
{
72+
result = s;
73+
}
74+
75+
return result;
76+
}
77+
}

src/Wpf.Ui/Controls/Menu/MenuItem.xaml

+42
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@
7474
ContentSource="Header"
7575
RecognizesAccessKey="True"
7676
TextElement.Foreground="{TemplateBinding Foreground}" />
77+
<!-- TextBlock added so that screen readers don't try to read Icon -->
78+
<TextBlock Grid.Column="0"
79+
Grid.ColumnSpan="3"
80+
Width="{Binding Width, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
81+
Height="{Binding Height, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
82+
Margin="-10"
83+
FontSize="1"
84+
Opacity="0"
85+
Text="{Binding AutomationProperties.Name, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
86+
Background="Black"/>
7787
</Grid>
7888

7989
<Popup
@@ -181,6 +191,16 @@
181191
ContentSource="Header"
182192
RecognizesAccessKey="True"
183193
TextElement.Foreground="{TemplateBinding Foreground}" />
194+
<!-- TextBlock added so that screen readers don't try to read Icon -->
195+
<TextBlock Grid.Column="0"
196+
Grid.ColumnSpan="3"
197+
Width="{Binding Width, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
198+
Height="{Binding Height, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
199+
Margin="-10"
200+
FontSize="1"
201+
Opacity="0"
202+
Text="{Binding AutomationProperties.Name, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
203+
Background="Black"/>
184204
</Grid>
185205
</Border>
186206
<ControlTemplate.Triggers>
@@ -264,6 +284,17 @@
264284
FontSize="11"
265285
Foreground="{DynamicResource TextFillColorDisabledBrush}"
266286
Text="{TemplateBinding InputGestureText}" />
287+
288+
<!-- TextBlock added so that screen readers don't try to read Icon -->
289+
<TextBlock Grid.Column="0"
290+
Grid.ColumnSpan="5"
291+
Width="{Binding Width, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
292+
Height="{Binding Height, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
293+
Margin="-10"
294+
FontSize="1"
295+
Opacity="0"
296+
Text="{Binding AutomationProperties.Name, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
297+
Background="Black"/>
267298
</Grid>
268299
</Border>
269300
<ControlTemplate.Triggers>
@@ -334,6 +365,17 @@
334365
FontSize="{TemplateBinding FontSize}"
335366
Symbol="ChevronRight20" />
336367
</Grid>
368+
369+
<!-- Fix double narration from mousing over the button and then the associated text -->
370+
<TextBlock Grid.Column="0"
371+
Grid.ColumnSpan="3"
372+
Width="{Binding Width, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
373+
Height="{Binding Height, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
374+
Margin="-10"
375+
FontSize="1"
376+
Opacity="0"
377+
Text="{Binding AutomationProperties.Name, RelativeSource={RelativeSource AncestorType={x:Type MenuItem}}}"
378+
Background="Black"/>
337379
</Grid>
338380
</Border>
339381

src/Wpf.Ui/Controls/NavigationView/NavigationViewItem.cs

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections;
99
using System.Collections.ObjectModel;
1010
using System.Collections.Specialized;
11+
using System.Windows.Automation.Peers;
1112
using System.Windows.Controls;
1213
using System.Windows.Input;
1314

@@ -438,6 +439,11 @@ protected override void OnMouseDown(MouseButtonEventArgs e)
438439
e.Handled = true;
439440
}
440441

442+
protected override AutomationPeer OnCreateAutomationPeer()
443+
{
444+
return new NavigationViewItemAutomationPeer(this);
445+
}
446+
441447
private void OnMenuItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
442448
{
443449
SetValue(HasMenuItemsPropertyKey, MenuItems.Count > 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// This Source Code Form is subject to the terms of the MIT License.
2+
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
3+
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
4+
// All Rights Reserved.
5+
6+
using System.Windows.Automation;
7+
using System.Windows.Automation.Peers;
8+
9+
namespace Wpf.Ui.Controls;
10+
11+
internal class NavigationViewItemAutomationPeer : FrameworkElementAutomationPeer
12+
{
13+
private readonly NavigationViewItem _owner;
14+
15+
public NavigationViewItemAutomationPeer(NavigationViewItem owner)
16+
: base(owner)
17+
{
18+
_owner = owner;
19+
}
20+
21+
protected override string GetClassNameCore()
22+
{
23+
return "NavigationItem";
24+
}
25+
26+
protected override AutomationControlType GetAutomationControlTypeCore()
27+
{
28+
return AutomationControlType.TabItem;
29+
}
30+
31+
public override object GetPattern(PatternInterface patternInterface)
32+
{
33+
if (patternInterface == PatternInterface.ItemContainer)
34+
{
35+
return this;
36+
}
37+
38+
return base.GetPattern(patternInterface);
39+
}
40+
41+
protected override AutomationPeer GetLabeledByCore()
42+
{
43+
if (_owner.Content is UIElement element)
44+
{
45+
return CreatePeerForElement(element);
46+
}
47+
48+
return base.GetLabeledByCore();
49+
}
50+
51+
protected override string GetNameCore()
52+
{
53+
var result = base.GetNameCore() ?? string.Empty;
54+
55+
if (result == string.Empty)
56+
{
57+
result = AutomationProperties.GetName(_owner);
58+
}
59+
60+
if (result == string.Empty && _owner.Content is DependencyObject d)
61+
{
62+
result = AutomationProperties.GetName(d);
63+
}
64+
65+
if (result == string.Empty && _owner.Content is string s)
66+
{
67+
result = s;
68+
}
69+
70+
return result;
71+
}
72+
}

0 commit comments

Comments
 (0)