Skip to content

Commit ff915f0

Browse files
Fix many screen reader accessibility issues (#4666)
* Fix navigation bar accessibility for screen readers Added AutomationProperties.SetName to the CustomNavViewItem control to ensure screen readers correctly announce the localized names for navigation options like 'Software Updates' and 'Installed Packages'. * Fix comprehensive screen reader accessibility issues This commit resolves several UX issues to significantly improve the keyboard and screen reader navigation flow: - Added AutomationProperties.Name to the loading screen grid, operation list resizers, and the ExpandCollapseOpList button. - Disabled IsTabStop on the custom TitleBar so it doesn't get focused unnecessarily. - Fixed accessibility for view mode and filter options by assigning them grouping roles. - Added descriptive labels to the ToggleFiltersButton and MainToolbarButtonDropdown ('More actions'). - Bound AutomationProperties.Name to the dynamic text of MainToolbarButton so it correctly reads 'Install selected packages', etc. - Added AutomationProperties.Name to the BackButton on the package preferences dialog. - Added AutomationProperties.Name to the search mode radio buttons. - Removed 'ghost' tab stops following the Help button by explicitly setting IsTabStop='False' and OverflowButtonVisibility='Collapsed' on the CommandBar and surrounding layout containers. - Fixed CheckboxHeader by setting IsTabStop='False' and AccessibilityView='Raw'. - Improved package list item accessibility by building a custom AutomationPeer for PackageItemContainer that implements IToggleProvider and acts natively as a CheckBox role, allowing direct selection and status announcement without an extra tab stop. - Added AutomationProperties.Name and ToolTip labels ('User profile') to the dynamic GitHub login/cloud backup avatar UserControl in the main title bar. * Hide FiltersResizer from screen readers Set IsTabStop='False' and AccessibilityView='Raw' on the FiltersResizer to completely remove it from the keyboard navigation flow and accessibility tree, as it is purely visual and does not need to be focused by screen reader users.
1 parent dc4a286 commit ff915f0

8 files changed

Lines changed: 124 additions & 9 deletions

File tree

src/UniGetUI/Controls/PackageItemContainer.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using Microsoft.UI.Xaml.Automation;
2+
using Microsoft.UI.Xaml.Automation.Peers;
3+
using Microsoft.UI.Xaml.Automation.Provider;
14
using Microsoft.UI.Xaml.Controls;
25
using UniGetUI.PackageEngine.Interfaces;
36
using UniGetUI.PackageEngine.PackageClasses;
@@ -7,6 +10,78 @@ namespace UniGetUI.Interface.Widgets
710
public partial class PackageItemContainer : ItemContainer
811
{
912
public IPackage? Package { get; set; }
10-
public PackageWrapper Wrapper { get; set; } = null!;
13+
14+
private PackageWrapper _wrapper = null!;
15+
public PackageWrapper Wrapper
16+
{
17+
get => _wrapper;
18+
set
19+
{
20+
if (_wrapper != null)
21+
{
22+
_wrapper.PropertyChanged -= Wrapper_PropertyChanged;
23+
}
24+
_wrapper = value;
25+
if (_wrapper != null)
26+
{
27+
_wrapper.PropertyChanged += Wrapper_PropertyChanged;
28+
}
29+
}
30+
}
31+
32+
private void Wrapper_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
33+
{
34+
if (e.PropertyName == nameof(PackageWrapper.IsChecked))
35+
{
36+
var peer = FrameworkElementAutomationPeer.FromElement(this) as PackageItemContainerAutomationPeer;
37+
if (peer != null)
38+
{
39+
ToggleState oldState = !Wrapper.IsChecked ? ToggleState.On : ToggleState.Off;
40+
ToggleState newState = Wrapper.IsChecked ? ToggleState.On : ToggleState.Off;
41+
peer.RaiseToggleStatePropertyChanged(oldState, newState);
42+
}
43+
}
44+
}
45+
46+
protected override AutomationPeer OnCreateAutomationPeer()
47+
{
48+
return new PackageItemContainerAutomationPeer(this);
49+
}
50+
}
51+
52+
public partial class PackageItemContainerAutomationPeer : FrameworkElementAutomationPeer, IToggleProvider
53+
{
54+
private readonly PackageItemContainer _owner;
55+
56+
public PackageItemContainerAutomationPeer(PackageItemContainer owner) : base(owner)
57+
{
58+
_owner = owner;
59+
}
60+
61+
protected override AutomationControlType GetAutomationControlTypeCore()
62+
{
63+
return AutomationControlType.CheckBox;
64+
}
65+
66+
protected override object GetPatternCore(PatternInterface patternInterface)
67+
{
68+
if (patternInterface == PatternInterface.Toggle)
69+
{
70+
return this;
71+
}
72+
return base.GetPatternCore(patternInterface);
73+
}
74+
75+
public ToggleState ToggleState => (_owner.Wrapper != null && _owner.Wrapper.IsChecked) ? ToggleState.On : ToggleState.Off;
76+
77+
public void Toggle()
78+
{
79+
_owner.Wrapper?.IsChecked = !_owner.Wrapper.IsChecked;
80+
}
81+
82+
public void RaiseToggleStatePropertyChanged(ToggleState oldValue, ToggleState newValue)
83+
{
84+
RaisePropertyChangedEvent(TogglePatternIdentifiers.ToggleStateProperty, oldValue, newValue);
85+
}
1186
}
1287
}

src/UniGetUI/Controls/PackageWrapper.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ public bool IsChecked
3131
{
3232
Package.IsChecked = value;
3333
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
34+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CheckedStatus)));
3435
_page.UpdatePackageCount();
3536
}
3637
}
3738

39+
public string CheckedStatus => IsChecked ? CoreTools.Translate("Checked") : CoreTools.Translate("Unchecked");
40+
3841
public bool IconWasLoaded;
3942
public bool AlternateIdIconVisible;
4043
public bool ShowCustomPackageIcon;

src/UniGetUI/MainWindow.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
PaneToggleRequested="TitleBar_PaneToggleRequested"
3737
SizeChanged="TitleBar_SizeChanged"
3838
Visibility="Collapsed"
39+
IsTabStop="False"
3940
>
4041
<!--TitleBar.IconSource>
4142
<ImageIconSource ImageSource="ms-appx:///Assets/Images/icon.png" />
@@ -97,7 +98,7 @@
9798
/>
9899
</StackPanel>
99100
<Frame Name="MainContentFrame" Grid.Row="2">
100-
<Grid Background="{StaticResource ProgressBarBorderThemeBrush}" Visibility="Visible">
101+
<Grid Background="{StaticResource ProgressBarBorderThemeBrush}" Visibility="Visible" AutomationProperties.Name="Loading UniGetUI">
101102
<Grid.ColumnDefinitions>
102103
<ColumnDefinition Width="*" />
103104
<ColumnDefinition Width="Auto" />

src/UniGetUI/Pages/MainView.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@
381381

382382
<controls:GridSplitter
383383
x:Name="OperationSplitter"
384+
AutomationProperties.Name="Operation list resizer"
384385
Grid.Row="1"
385386
Grid.Column="0"
386387
Height="12"
@@ -398,6 +399,7 @@
398399
</Grid.ColumnDefinitions>
399400
<HyperlinkButton
400401
Name="ExpandCollapseOpList"
402+
AutomationProperties.Name="Expand or collapse operation list"
401403
Grid.Column="1"
402404
Height="12"
403405
Padding="12,0,12,0"

src/UniGetUI/Pages/SettingsPages/GeneralPages/Updates.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ private void RefreshMinimumAgeLayout()
9898
: new CornerRadius(8);
9999
}
100100

101-
private UIElement BuildReleaseDateCompatTable()
101+
private static UIElement BuildReleaseDateCompatTable()
102102
{
103103
string yesStr = CoreTools.Translate("Yes");
104104
string noStr = CoreTools.Translate("No");

src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
</Grid.ColumnDefinitions>
4545
<Button
4646
Name="BackButton"
47+
AutomationProperties.Name="Back"
4748
Width="36"
4849
Height="36"
4950
Padding="6"

src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
Grid.Column="0"
4949
VerticalAlignment="Center"
5050
IsChecked="{x:Bind IsChecked, Mode=TwoWay}"
51+
IsTabStop="False"
52+
AutomationProperties.AccessibilityView="Raw"
5153
/>
5254

5355
<!-- Regular or package icon -->
@@ -285,6 +287,8 @@
285287
HorizontalAlignment="Left"
286288
VerticalAlignment="Top"
287289
IsChecked="{x:Bind IsChecked, Mode=TwoWay}"
290+
IsTabStop="False"
291+
AutomationProperties.AccessibilityView="Raw"
288292
/>
289293

290294
<Button
@@ -418,6 +422,8 @@
418422
HorizontalAlignment="Left"
419423
VerticalAlignment="Top"
420424
IsChecked="{x:Bind IsChecked, Mode=TwoWay}"
425+
IsTabStop="False"
426+
AutomationProperties.AccessibilityView="Raw"
421427
/>
422428
<Button
423429
Grid.Row="0"
@@ -591,24 +597,26 @@
591597
VerticalAlignment="Center"
592598
Orientation="Horizontal"
593599
Spacing="4"
600+
AutomationProperties.Name="View modes"
601+
AutomationProperties.LocalizedControlType="grouping"
594602
>
595603
<widgets:TranslatedTextBlock VerticalAlignment="Center" Text="View mode:" />
596604
<Toolkit:Segmented
597605
x:Name="ViewModeSelector"
598606
SelectionChanged="ViewModeSelector_SelectionChanged"
599607
SelectionMode="Single"
600608
>
601-
<Toolkit:SegmentedItem x:Name="Selector_List">
609+
<Toolkit:SegmentedItem x:Name="Selector_List" AutomationProperties.Name="List view">
602610
<Toolkit:SegmentedItem.Icon>
603611
<FontIcon Glyph="&#xE8FD;" />
604612
</Toolkit:SegmentedItem.Icon>
605613
</Toolkit:SegmentedItem>
606-
<Toolkit:SegmentedItem x:Name="Selector_Grid">
614+
<Toolkit:SegmentedItem x:Name="Selector_Grid" AutomationProperties.Name="Grid view">
607615
<Toolkit:SegmentedItem.Icon>
608616
<FontIcon Glyph="&#xF168;" />
609617
</Toolkit:SegmentedItem.Icon>
610618
</Toolkit:SegmentedItem>
611-
<Toolkit:SegmentedItem x:Name="Selector_Icons">
619+
<Toolkit:SegmentedItem x:Name="Selector_Icons" AutomationProperties.Name="Icons view">
612620
<Toolkit:SegmentedItem.Icon>
613621
<FontIcon Glyph="&#xF0E2;" />
614622
</Toolkit:SegmentedItem.Icon>
@@ -641,6 +649,7 @@
641649
<StackPanel x:Name="ToggleFiltersButtonWidth" Margin="0,0,13,0" Orientation="Horizontal">
642650
<ToggleButton
643651
x:Name="ToggleFiltersButton"
652+
AutomationProperties.Name="Toggle filters panel"
644653
Grid.Column="0"
645654
Height="36"
646655
Margin="1,4"
@@ -688,6 +697,7 @@
688697
<StackPanel Grid.Column="1" Orientation="Horizontal">
689698
<Button
690699
x:Name="MainToolbarButton"
700+
AutomationProperties.Name="{x:Bind MainToolbarButtonText.Text, Mode=OneWay}"
691701
Height="36"
692702
Margin="0,0,1,0"
693703
x:FieldModifier="protected"
@@ -711,6 +721,7 @@
711721
</Button>
712722
<Button
713723
x:Name="MainToolbarButtonDropdown"
724+
AutomationProperties.Name="More actions"
714725
Width="30"
715726
Height="36"
716727
Padding="4"
@@ -728,6 +739,8 @@
728739
HorizontalAlignment="Left"
729740
x:FieldModifier="protected"
730741
DefaultLabelPosition="Right"
742+
IsDynamicOverflowEnabled="False"
743+
OverflowButtonVisibility="Collapsed"
731744
/>
732745
</Grid>
733746
</Grid>
@@ -747,6 +760,7 @@
747760
PaneClosing="FilteringPanel_PaneClosing"
748761
PanePlacement="Left"
749762
SizeChanged="FilteringPanel_SizeChanged"
763+
IsTabStop="False"
750764
>
751765
<SplitView.Pane>
752766
<ScrollViewer
@@ -759,6 +773,7 @@
759773
CornerRadius="0,8,8,0"
760774
HorizontalScrollMode="Disabled"
761775
SizeChanged="SidepanelWidth_SizeChanged"
776+
IsTabStop="False"
762777
>
763778
<Grid Name="SidePanelGrid" RowSpacing="8">
764779
<Grid.RowDefinitions>
@@ -887,7 +902,7 @@
887902
</StackPanel>
888903
</Expander.Header>
889904
<Expander.Content>
890-
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical" Spacing="0">
905+
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical" Spacing="0" AutomationProperties.Name="Filter options" AutomationProperties.LocalizedControlType="grouping">
891906
<CheckBox
892907
x:Name="InstantSearchCheckbox"
893908
x:FieldModifier="protected"
@@ -959,6 +974,7 @@
959974
>
960975
<RadioButton
961976
x:Name="QueryNameRadio"
977+
AutomationProperties.Name="Package Name"
962978
Height="30"
963979
Margin="0,-2,0,-2"
964980
x:FieldModifier="protected"
@@ -969,6 +985,7 @@
969985
</RadioButton>
970986
<RadioButton
971987
x:Name="QueryIdRadio"
988+
AutomationProperties.Name="Package ID"
972989
Height="30"
973990
Margin="0,-2,0,-2"
974991
x:FieldModifier="protected"
@@ -979,6 +996,7 @@
979996
</RadioButton>
980997
<RadioButton
981998
x:Name="QueryBothRadio"
999+
AutomationProperties.Name="Both"
9821000
Height="30"
9831001
Margin="0,-2,0,-2"
9841002
x:FieldModifier="protected"
@@ -990,6 +1008,7 @@
9901008
</RadioButton>
9911009
<RadioButton
9921010
x:Name="QueryExactMatch"
1011+
AutomationProperties.Name="Exact match"
9931012
Height="30"
9941013
Margin="0,-2,0,-2"
9951014
x:FieldModifier="protected"
@@ -1000,6 +1019,7 @@
10001019
</RadioButton>
10011020
<RadioButton
10021021
x:Name="QuerySimilarResultsRadio"
1022+
AutomationProperties.Name="Show similar packages"
10031023
Height="30"
10041024
Margin="0,-2,0,-2"
10051025
x:FieldModifier="protected"
@@ -1070,9 +1090,12 @@
10701090
HorizontalAlignment="Stretch"
10711091
BorderThickness="0"
10721092
CornerRadius="4,0,0,4"
1093+
IsTabStop="False"
1094+
AutomationProperties.AccessibilityView="Raw"
10731095
>
10741096
<CheckBox
10751097
Name="SelectAllCheckBox"
1098+
AutomationProperties.Name="Select all packages"
10761099
Margin="12,4,4,4"
10771100
Checked="SelectAllCheckBox_ValueChanged"
10781101
Unchecked="SelectAllCheckBox_ValueChanged"
@@ -1210,6 +1233,8 @@
12101233
Binding="{x:Bind FilteringPanel.OpenPaneLength, Mode=TwoWay}"
12111234
CornerRadius="2"
12121235
Visibility="{x:Bind FilteringPanel.IsPaneOpen, Mode=OneWay}"
1236+
IsTabStop="False"
1237+
AutomationProperties.AccessibilityView="Raw"
12131238
>
12141239
<Toolkit:PropertySizer.RenderTransform>
12151240
<TranslateTransform X="0" />

src/UniGetUI/Services/UserAvatar.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public UserAvatar()
2929
{
3030
VerticalContentAlignment = VerticalAlignment.Center;
3131
HorizontalContentAlignment = HorizontalAlignment.Center;
32+
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(this, CoreTools.Translate("User profile"));
33+
ToolTipService.SetToolTip(this, CoreTools.Translate("User profile"));
3234
_ = RefreshStatus();
3335
GitHubAuthService.AuthStatusChanged += GitHubAuthService_AuthStatusChanged;
3436
}
@@ -166,7 +168,7 @@ private PointButton GenerateLoginControl()
166168
Content = stackPanel,
167169
};
168170

169-
return new PointButton
171+
var btn = new PointButton
170172
{
171173
Margin = new Thickness(0),
172174
Padding = new Thickness(4),
@@ -176,6 +178,9 @@ private PointButton GenerateLoginControl()
176178
Content = personPicture,
177179
Flyout = flyout,
178180
};
181+
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(btn, CoreTools.Translate("User profile"));
182+
ToolTipService.SetToolTip(btn, CoreTools.Translate("User profile"));
183+
return btn;
179184
}
180185

181186
private async Task<PointButton> GenerateLogoutControl()
@@ -289,7 +294,7 @@ private async Task<PointButton> GenerateLogoutControl()
289294
Content = stackPanel,
290295
};
291296

292-
return new PointButton
297+
var btn = new PointButton
293298
{
294299
Margin = new Thickness(0),
295300
Padding = new Thickness(4),
@@ -299,6 +304,9 @@ private async Task<PointButton> GenerateLogoutControl()
299304
Content = personPicture,
300305
Flyout = flyout,
301306
};
307+
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(btn, CoreTools.Translate("User profile"));
308+
ToolTipService.SetToolTip(btn, CoreTools.Translate("User profile"));
309+
return btn;
302310
}
303311
}
304312
}

0 commit comments

Comments
 (0)