diff --git a/src/UniGetUI.Core.Tools.Tests/AccessibilityXamlTests.cs b/src/UniGetUI.Core.Tools.Tests/AccessibilityXamlTests.cs new file mode 100644 index 0000000000..94edba5001 --- /dev/null +++ b/src/UniGetUI.Core.Tools.Tests/AccessibilityXamlTests.cs @@ -0,0 +1,247 @@ +using System.Text.RegularExpressions; + +namespace UniGetUI.Core.Tools.Tests; + +public class AccessibilityXamlTests +{ + private static readonly string RepoRoot = + Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, @"..\..\..\..\..\..\..")); + + private static string ReadFile(string relativePath) + { + string path = Path.Combine(RepoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); + return File.ReadAllText(path); + } + + [Fact] + public void IconOnlyControlsHaveAutomationNames() + { + Dictionary requiredSnippets = new() + { + ["src/UniGetUI/Controls/DialogCloseButton.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind CloseAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Controls/SourceManager.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind RemoveSourceAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ReloadSourcesAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/HelpPage.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind HelpBackAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind HelpForwardAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind HelpHomeAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind HelpReloadAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/MainView.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind OperationOptionsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind OperationsListActionsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ExpandCollapseOperationsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ResizeOperationsAreaAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind BackAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind DeleteShortcutAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind OpenShortcutLocationAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind RemoveShortcutFromListAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind StopIgnoringUpdatesAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/DialogPages/PackageDetailsPage.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind MorePackageActionsAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/DialogPages/OperationFailedDialog.xaml"] = + [ + "AutomationProperties.Name=\"Command-line output\"" + ], + ["src/UniGetUI/Pages/DialogPages/OperationLiveLogPage.xaml"] = + [ + "AutomationProperties.Name=\"Operation live log output\"" + ], + ["src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind PackageOptionsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ReloadPackagesAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind MoreToolbarActionsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SelectPackageAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind OrderByAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ViewModeAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchPackagesAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SelectAllSourcesAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ClearSourceSelectionAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind InstantSearchAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind DistinguishUpperLowerCaseAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind IgnoreSpecialCharactersAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind ToggleFiltersAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind MainSelectionActionAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeOptionsAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeByNameAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeByIdAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeByBothAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeExactMatchAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind SearchModeSimilarResultsAutomationName, Mode=OneWay}\"" + ], + ["src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml"] = + [ + "AutomationProperties.Name=\"{x:Bind ResetBackupDirectoryAutomationName, Mode=OneWay}\"", + "AutomationProperties.Name=\"{x:Bind OpenBackupDirectoryAutomationName, Mode=OneWay}\"" + ] + }; + + foreach (var (file, snippets) in requiredSnippets) + { + string content = ReadFile(file); + foreach (string snippet in snippets) + { + Assert.Contains(snippet, content); + } + } + } + + [Fact] + public void CriticalListTemplatesHaveAutomationNames() + { + Dictionary requiredSnippets = new() + { + ["src/UniGetUI/Controls/SourceManager.xaml"] = + [ + "" + ], + ["src/UniGetUI/Pages/AboutPages/Contributors.xaml"] = + [ + "" + ], + ["src/UniGetUI/Pages/AboutPages/Translators.xaml"] = + [ + "" + ], + ["src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml"] = + [ + "" + ], + ["src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml"] = + [ + "" + ], + ["src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml"] = + [ + "" + ] + }; + + foreach (var (file, snippets) in requiredSnippets) + { + string content = ReadFile(file); + foreach (string snippet in snippets) + { + Assert.Contains(snippet, content); + } + } + } + + [Fact] + public void CriticalItemContainersAreNamed() + { + string[] files = + [ + "src/UniGetUI/Controls/SourceManager.xaml", + "src/UniGetUI/Pages/AboutPages/Contributors.xaml", + "src/UniGetUI/Pages/AboutPages/Translators.xaml", + "src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml", + "src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml", + "src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml", + "src/UniGetUI/Pages/MainView.xaml" + ]; + + foreach (string file in files) + { + string content = ReadFile(file); + MatchCollection matches = Regex.Matches(content, "]*>", RegexOptions.Singleline); + Assert.NotEmpty(matches); + + foreach (Match match in matches) + { + Assert.Contains("AutomationProperties.Name", match.Value); + } + } + } + + [Fact] + public void AccessibilityCriticalCodePathsSetNamesAndTabFocus() + { + Dictionary requiredSnippets = new() + { + ["src/UniGetUI/Controls/CustomNavViewItem.cs"] = + [ + "AutomationProperties.SetName(this, text);" + ], + ["src/UniGetUI/Services/UserAvatar.cs"] = + [ + "AutomationProperties.SetName(profileButton, CoreTools.Translate(\"Open backup profile\"));" + ], + ["src/UniGetUI/Controls/SettingsWidgets/SettingsPageButton.cs"] = + [ + "AutomationProperties.SetName(this, name);", + "IsTabStop = true;", + "UseSystemFocusVisuals = true;" + ], + ["src/UniGetUI/Controls/SettingsWidgets/CheckboxCard.cs"] = + [ + "AutomationProperties.SetName(_checkbox, name);", + "AutomationProperties.SetName(this, name);" + ], + ["src/UniGetUI/Controls/SettingsWidgets/SecureCheckboxCard.cs"] = + [ + "AutomationProperties.SetName(_checkbox, name);", + "AutomationProperties.SetName(this, name);" + ], + ["src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs"] = + [ + "AutomationProperties.SetName(_checkbox, _translatedCheckboxText);", + "AutomationProperties.SetName(Button, buttonName);" + ], + ["src/UniGetUI/Controls/SettingsWidgets/ButtonCard.cs"] = + [ + "AutomationProperties.SetName(_button, buttonName);", + "AutomationProperties.SetName(this, cardName);" + ], + ["src/UniGetUI/Controls/SettingsWidgets/ComboboxCard.cs"] = + [ + "AutomationProperties.SetName(_combobox, _translatedText);", + "AutomationProperties.SetName(this, _translatedText);" + ], + ["src/UniGetUI/Controls/SettingsWidgets/TextboxCard.cs"] = + [ + "AutomationProperties.SetName(_textbox, textboxName);", + "AutomationProperties.SetName(this, textboxName);" + ], + ["src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml.cs"] = + [ + "BackButton.PreviewKeyDown += BackButton_PreviewKeyDown;", + "root.TabFocusNavigation = KeyboardNavigationMode.Local;", + "MainNavigationFrame.TabFocusNavigation = KeyboardNavigationMode.Local;", + "scroller.TabFocusNavigation = KeyboardNavigationMode.Local;", + "NeedsContextualName(currentName, toggle.OnContent, toggle.OffContent)", + "AutomationProperties.SetName(card, cardName);" + ] + }; + + foreach (var (file, snippets) in requiredSnippets) + { + string content = ReadFile(file); + foreach (string snippet in snippets) + { + Assert.Contains(snippet, content); + } + } + } +} diff --git a/src/UniGetUI/Controls/CustomNavViewItem.cs b/src/UniGetUI/Controls/CustomNavViewItem.cs index 3788f57bcb..f8127d6845 100644 --- a/src/UniGetUI/Controls/CustomNavViewItem.cs +++ b/src/UniGetUI/Controls/CustomNavViewItem.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using UniGetUI.Core.Tools; using UniGetUI.Interface; @@ -44,6 +45,7 @@ public string Text { string text = CoreTools.Translate(value); _textBlock.Text = text; + AutomationProperties.SetName(this, text); ToolTipService.SetToolTip(this, text); } diff --git a/src/UniGetUI/Controls/DialogCloseButton.xaml b/src/UniGetUI/Controls/DialogCloseButton.xaml index 63a2c58994..38fd473933 100644 --- a/src/UniGetUI/Controls/DialogCloseButton.xaml +++ b/src/UniGetUI/Controls/DialogCloseButton.xaml @@ -18,6 +18,7 @@ Width="46" Height="31" Padding="0" + AutomationProperties.Name="{x:Bind CloseAutomationName, Mode=OneWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent" diff --git a/src/UniGetUI/Controls/DialogCloseButton.xaml.cs b/src/UniGetUI/Controls/DialogCloseButton.xaml.cs index 68f3ce6ee1..0fec8f426b 100644 --- a/src/UniGetUI/Controls/DialogCloseButton.xaml.cs +++ b/src/UniGetUI/Controls/DialogCloseButton.xaml.cs @@ -1,5 +1,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using UniGetUI.Core.Tools; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -8,6 +9,7 @@ namespace UniGetUI.Interface.Widgets; public sealed partial class DialogCloseButton : UserControl { public event EventHandler? Click; + public string CloseAutomationName => CoreTools.Translate("Close"); public DialogCloseButton() { diff --git a/src/UniGetUI/Controls/OperationWidgets/OperationControl.cs b/src/UniGetUI/Controls/OperationWidgets/OperationControl.cs index 397393388b..88058effb1 100644 --- a/src/UniGetUI/Controls/OperationWidgets/OperationControl.cs +++ b/src/UniGetUI/Controls/OperationWidgets/OperationControl.cs @@ -30,6 +30,7 @@ public partial class OperationControl: INotifyPropertyChanged public AbstractOperation Operation; public BetterMenu OpMenu; public OperationStatus? MenuStateOnLoaded; + public string OperationOptionsAutomationName => CoreTools.Translate("Operation options"); public ObservableCollection Badges = []; private int _errorCount = 0; diff --git a/src/UniGetUI/Controls/PackageWrapper.cs b/src/UniGetUI/Controls/PackageWrapper.cs index ca9edc465b..2f2251bf98 100644 --- a/src/UniGetUI/Controls/PackageWrapper.cs +++ b/src/UniGetUI/Controls/PackageWrapper.cs @@ -59,6 +59,8 @@ public Uri? PackageIcon public int NewVersionLabelWidth { get => Package.IsUpgradable ? 125 : 0; } public int NewVersionIconWidth { get => Package.IsUpgradable ? 24 : 0; } + public string PackageOptionsAutomationName => CoreTools.Translate("Package options"); + public string SelectPackageAutomationName => CoreTools.Translate("Select package {0}", Package.Name); public int Index { get; set; } public event PropertyChangedEventHandler? PropertyChanged; diff --git a/src/UniGetUI/Controls/SettingsWidgets/ButtonCard.cs b/src/UniGetUI/Controls/SettingsWidgets/ButtonCard.cs index e7e54f8c68..39d7f27274 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/ButtonCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/ButtonCard.cs @@ -1,4 +1,5 @@ using CommunityToolkit.WinUI.Controls; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using UniGetUI.Core.Tools; @@ -10,15 +11,51 @@ namespace UniGetUI.Interface.Widgets public sealed partial class ButtonCard : SettingsCard { private readonly Button _button = new(); + private string _translatedText = string.Empty; + private string _translatedButtonText = string.Empty; + + private void UpdateAutomationNames() + { + if (!string.IsNullOrWhiteSpace(_translatedButtonText)) + { + string buttonName = string.IsNullOrWhiteSpace(_translatedText) + ? _translatedButtonText + : $"{_translatedButtonText}. {_translatedText}"; + AutomationProperties.SetName(_button, buttonName); + } + + string cardName = _translatedText; + if (!string.IsNullOrWhiteSpace(_translatedButtonText)) + { + cardName = string.IsNullOrWhiteSpace(cardName) + ? _translatedButtonText + : $"{cardName}. {_translatedButtonText}"; + } + + if (!string.IsNullOrWhiteSpace(cardName)) + { + AutomationProperties.SetName(this, cardName); + } + } public string ButtonText { - set => _button.Content = CoreTools.Translate(value); + set + { + _translatedButtonText = CoreTools.Translate(value); + _button.Content = _translatedButtonText; + UpdateAutomationNames(); + } } public string Text { - set => Header = CoreTools.Translate(value); + set + { + _translatedText = CoreTools.Translate(value); + Header = _translatedText; + UpdateAutomationNames(); + } } public new event EventHandler? Click; @@ -28,6 +65,7 @@ public ButtonCard() _button.MinWidth = 200; _button.Click += (_, _) => { Click?.Invoke(this, EventArgs.Empty); }; Content = _button; + UpdateAutomationNames(); } } } diff --git a/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs b/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs index 09846f75a3..bc116547cd 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs @@ -1,5 +1,6 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Media; @@ -18,6 +19,37 @@ public sealed partial class CheckboxButtonCard : SettingsCard public TextBlock _textblock; public ButtonBase Button; private bool IS_INVERTED; + private string _translatedCheckboxText = string.Empty; + private string _translatedButtonText = string.Empty; + + private void UpdateAutomationNames() + { + if (!string.IsNullOrWhiteSpace(_translatedCheckboxText)) + { + AutomationProperties.SetName(_checkbox, _translatedCheckboxText); + } + + if (!string.IsNullOrWhiteSpace(_translatedButtonText)) + { + string buttonName = string.IsNullOrWhiteSpace(_translatedCheckboxText) + ? _translatedButtonText + : $"{_translatedButtonText}. {_translatedCheckboxText}"; + AutomationProperties.SetName(Button, buttonName); + } + + string cardName = _translatedCheckboxText; + if (!string.IsNullOrWhiteSpace(_translatedButtonText)) + { + cardName = string.IsNullOrWhiteSpace(cardName) + ? _translatedButtonText + : $"{cardName}. {_translatedButtonText}"; + } + + if (!string.IsNullOrWhiteSpace(cardName)) + { + AutomationProperties.SetName(this, cardName); + } + } private Settings.K setting_name = Settings.K.Unset; public Settings.K SettingName @@ -42,12 +74,22 @@ public bool Checked public string CheckboxText { - set => _textblock.Text = CoreTools.Translate(value); + set + { + _translatedCheckboxText = CoreTools.Translate(value); + _textblock.Text = _translatedCheckboxText; + UpdateAutomationNames(); + } } public string ButtonText { - set => Button.Content = CoreTools.Translate(value); + set + { + _translatedButtonText = CoreTools.Translate(value); + Button.Content = _translatedButtonText; + UpdateAutomationNames(); + } } private bool _buttonAlwaysOn; @@ -96,6 +138,7 @@ public CheckboxButtonCard() }; Button.Click += (s, e) => Click?.Invoke(s, e); + UpdateAutomationNames(); } } } diff --git a/src/UniGetUI/Controls/SettingsWidgets/CheckboxCard.cs b/src/UniGetUI/Controls/SettingsWidgets/CheckboxCard.cs index 51fd5a4ae8..3c83bb668c 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/CheckboxCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/CheckboxCard.cs @@ -1,5 +1,6 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using UniGetUI.Core.SettingsEngine; @@ -16,6 +17,25 @@ public partial class CheckboxCard : SettingsCard public TextBlock _textblock; public TextBlock _warningBlock; protected bool IS_INVERTED; + private string _translatedText = string.Empty; + private string _translatedWarningText = string.Empty; + + private void UpdateAutomationName() + { + string name = _translatedText; + if (!string.IsNullOrWhiteSpace(_translatedWarningText)) + { + name = string.IsNullOrWhiteSpace(name) + ? _translatedWarningText + : $"{name}. {_translatedWarningText}"; + } + + if (!string.IsNullOrWhiteSpace(name)) + { + AutomationProperties.SetName(_checkbox, name); + AutomationProperties.SetName(this, name); + } + } private Settings.K setting_name = Settings.K.Unset; public Settings.K SettingName @@ -39,15 +59,22 @@ public bool Checked public string Text { - set => _textblock.Text = CoreTools.Translate(value); + set + { + _translatedText = CoreTools.Translate(value); + _textblock.Text = _translatedText; + UpdateAutomationName(); + } } public string WarningText { set { - _warningBlock.Text = CoreTools.Translate(value); + _translatedWarningText = CoreTools.Translate(value); + _warningBlock.Text = _translatedWarningText; _warningBlock.Visibility = value.Any() ? Visibility.Visible : Visibility.Collapsed; + UpdateAutomationName(); } } @@ -95,6 +122,7 @@ public CheckboxCard() _checkbox.HorizontalAlignment = HorizontalAlignment.Stretch; _checkbox.Toggled += _checkbox_Toggled; + UpdateAutomationName(); } protected virtual void _checkbox_Toggled(object sender, RoutedEventArgs e) { diff --git a/src/UniGetUI/Controls/SettingsWidgets/ComboboxCard.cs b/src/UniGetUI/Controls/SettingsWidgets/ComboboxCard.cs index 3c29cc974d..0c9e5491d7 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/ComboboxCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/ComboboxCard.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using CommunityToolkit.WinUI.Controls; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; using UniGetUI.Core.Logging; @@ -17,6 +18,16 @@ public sealed partial class ComboboxCard : SettingsCard private readonly ObservableCollection _elements = []; private readonly Dictionary _values_ref = []; private readonly Dictionary _inverted_val_ref = []; + private string _translatedText = string.Empty; + + private void UpdateAutomationNames() + { + if (!string.IsNullOrWhiteSpace(_translatedText)) + { + AutomationProperties.SetName(_combobox, _translatedText); + AutomationProperties.SetName(this, _translatedText); + } + } private Settings.K settings_name = Settings.K.Unset; public Settings.K SettingName @@ -29,7 +40,12 @@ public Settings.K SettingName public string Text { - set => Header = CoreTools.Translate(value); + set + { + _translatedText = CoreTools.Translate(value); + Header = _translatedText; + UpdateAutomationNames(); + } } public event EventHandler? ValueChanged; @@ -39,6 +55,7 @@ public ComboboxCard() _combobox.MinWidth = 200; _combobox.SetBinding(ItemsControl.ItemsSourceProperty, new Binding { Source = _elements }); Content = _combobox; + UpdateAutomationNames(); } public void AddItem(string name, string value) diff --git a/src/UniGetUI/Controls/SettingsWidgets/SecureCheckboxCard.cs b/src/UniGetUI/Controls/SettingsWidgets/SecureCheckboxCard.cs index d8f66c42b3..cfaf200c09 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/SecureCheckboxCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/SecureCheckboxCard.cs @@ -1,5 +1,6 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using UniGetUI.Core.Logging; @@ -18,6 +19,25 @@ public partial class SecureCheckboxCard : SettingsCard public TextBlock _warningBlock; public ProgressRing _loading; private bool IS_INVERTED; + private string _translatedText = string.Empty; + private string _translatedWarningText = string.Empty; + + private void UpdateAutomationName() + { + string name = _translatedText; + if (!string.IsNullOrWhiteSpace(_translatedWarningText)) + { + name = string.IsNullOrWhiteSpace(name) + ? _translatedWarningText + : $"{name}. {_translatedWarningText}"; + } + + if (!string.IsNullOrWhiteSpace(name)) + { + AutomationProperties.SetName(_checkbox, name); + AutomationProperties.SetName(this, name); + } + } private SecureSettings.K setting_name = SecureSettings.K.Unset; public SecureSettings.K SettingName @@ -53,15 +73,22 @@ public bool Checked public string Text { - set => _textblock.Text = CoreTools.Translate(value); + set + { + _translatedText = CoreTools.Translate(value); + _textblock.Text = _translatedText; + UpdateAutomationName(); + } } public string WarningText { set { - _warningBlock.Text = CoreTools.Translate(value); + _translatedWarningText = CoreTools.Translate(value); + _warningBlock.Text = _translatedWarningText; _warningBlock.Visibility = value.Any() ? Visibility.Visible : Visibility.Collapsed; + UpdateAutomationName(); } } @@ -107,6 +134,7 @@ public SecureCheckboxCard() _checkbox.HorizontalAlignment = HorizontalAlignment.Stretch; _checkbox.Toggled += (s, e) => _ = _checkbox_Toggled(); + UpdateAutomationName(); } protected virtual async Task _checkbox_Toggled() { diff --git a/src/UniGetUI/Controls/SettingsWidgets/SettingsPageButton.cs b/src/UniGetUI/Controls/SettingsWidgets/SettingsPageButton.cs index 78e6f892ae..720e03c186 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/SettingsPageButton.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/SettingsPageButton.cs @@ -1,5 +1,6 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; @@ -10,14 +11,35 @@ namespace UniGetUI.Interface.Widgets { public partial class SettingsPageButton : SettingsCard { + private string _headerText = string.Empty; + private string _descriptionText = string.Empty; + + private void UpdateAutomationName() + { + string name = string.IsNullOrWhiteSpace(_descriptionText) + ? _headerText + : $"{_headerText}. {_descriptionText}"; + AutomationProperties.SetName(this, name); + } + public string Text { - set => Header = CoreTools.Translate(value); + set + { + _headerText = CoreTools.Translate(value); + Header = _headerText; + UpdateAutomationName(); + } } public string UnderText { - set => Description = CoreTools.Translate(value); + set + { + _descriptionText = CoreTools.Translate(value); + Description = _descriptionText; + UpdateAutomationName(); + } } public IconType Icon @@ -30,6 +52,8 @@ public SettingsPageButton() CornerRadius = new CornerRadius(8); HorizontalAlignment = HorizontalAlignment.Stretch; IsClickEnabled = true; + IsTabStop = true; + UseSystemFocusVisuals = true; } } } diff --git a/src/UniGetUI/Controls/SettingsWidgets/TextboxCard.cs b/src/UniGetUI/Controls/SettingsWidgets/TextboxCard.cs index bb6b4cce5b..57bca4afc7 100644 --- a/src/UniGetUI/Controls/SettingsWidgets/TextboxCard.cs +++ b/src/UniGetUI/Controls/SettingsWidgets/TextboxCard.cs @@ -1,5 +1,6 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; @@ -13,6 +14,25 @@ public sealed partial class TextboxCard : SettingsCard { private readonly TextBox _textbox; private readonly HyperlinkButton _helpbutton; + private string _translatedText = string.Empty; + private string _translatedPlaceholder = string.Empty; + + private void UpdateAutomationNames() + { + string textboxName = _translatedText; + if (!string.IsNullOrWhiteSpace(_translatedPlaceholder)) + { + textboxName = string.IsNullOrWhiteSpace(textboxName) + ? _translatedPlaceholder + : $"{textboxName}. {_translatedPlaceholder}"; + } + + if (!string.IsNullOrWhiteSpace(textboxName)) + { + AutomationProperties.SetName(_textbox, textboxName); + AutomationProperties.SetName(this, textboxName); + } + } private Settings.K setting_name = Settings.K.Unset; public Settings.K SettingName @@ -26,12 +46,22 @@ public Settings.K SettingName public string Placeholder { - set => _textbox.PlaceholderText = CoreTools.Translate(value); + set + { + _translatedPlaceholder = CoreTools.Translate(value); + _textbox.PlaceholderText = _translatedPlaceholder; + UpdateAutomationNames(); + } } public string Text { - set => Header = CoreTools.Translate(value); + set + { + _translatedText = CoreTools.Translate(value); + Header = _translatedText; + UpdateAutomationNames(); + } } public Uri HelpUrl @@ -41,6 +71,7 @@ public Uri HelpUrl _helpbutton.NavigateUri = value; _helpbutton.Visibility = Visibility.Visible; _helpbutton.Content = CoreTools.Translate("More info"); + AutomationProperties.SetName(_helpbutton, CoreTools.Translate("More info")); } } @@ -68,6 +99,7 @@ public TextboxCard() s.Children.Add(_textbox); Content = s; + UpdateAutomationNames(); } public void SaveValue() diff --git a/src/UniGetUI/Controls/SourceManager.xaml b/src/UniGetUI/Controls/SourceManager.xaml index 72111d3590..5e5d1a28ef 100644 --- a/src/UniGetUI/Controls/SourceManager.xaml +++ b/src/UniGetUI/Controls/SourceManager.xaml @@ -11,56 +11,59 @@ mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -109,6 +112,7 @@ Height="30" Padding="0,0,0,0" HorizontalAlignment="Right" + AutomationProperties.Name="{x:Bind ReloadSourcesAutomationName, Mode=OneWay}" Click="ReloadButton_Click" CornerRadius="4"> CoreTools.Translate("Remove source"); public SourceItem(SourceManager Parent, IManagerSource Source) { @@ -36,6 +37,7 @@ public void Remove(object sender, RoutedEventArgs e) public sealed partial class SourceManager : UserControl { private IPackageManager Manager { get; set; } + public string ReloadSourcesAutomationName => CoreTools.Translate("Reload sources"); // ReSharper disable once FieldCanBeMadeReadOnly.Local private ObservableCollection Sources = []; diff --git a/src/UniGetUI/Pages/AboutPages/Contributors.xaml b/src/UniGetUI/Pages/AboutPages/Contributors.xaml index d8bf56c2d0..03cb816c40 100644 --- a/src/UniGetUI/Pages/AboutPages/Contributors.xaml +++ b/src/UniGetUI/Pages/AboutPages/Contributors.xaml @@ -26,44 +26,46 @@ - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + VerticalAlignment="Center" + Text="{x:Bind Name}" /> + + + + + diff --git a/src/UniGetUI/Pages/AboutPages/SupportMe.xaml b/src/UniGetUI/Pages/AboutPages/SupportMe.xaml index b63cfd5048..f2da97953f 100644 --- a/src/UniGetUI/Pages/AboutPages/SupportMe.xaml +++ b/src/UniGetUI/Pages/AboutPages/SupportMe.xaml @@ -28,7 +28,10 @@ - + diff --git a/src/UniGetUI/Pages/AboutPages/SupportMe.xaml.cs b/src/UniGetUI/Pages/AboutPages/SupportMe.xaml.cs index d302702b00..a979e72be8 100644 --- a/src/UniGetUI/Pages/AboutPages/SupportMe.xaml.cs +++ b/src/UniGetUI/Pages/AboutPages/SupportMe.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Xaml.Controls; +using UniGetUI.Core.Tools; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -10,6 +11,8 @@ namespace UniGetUI.Interface.Pages.AboutPages /// public sealed partial class SupportMe : Page { + public string SupportKofiAutomationName => CoreTools.Translate("Support the developer on Ko-fi"); + public SupportMe() { InitializeComponent(); diff --git a/src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml b/src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml index 7cac12be4e..ec6e49d86b 100644 --- a/src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml +++ b/src/UniGetUI/Pages/AboutPages/ThirdPartyLicenses.xaml @@ -35,33 +35,35 @@ - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/UniGetUI/Pages/AboutPages/Translators.xaml b/src/UniGetUI/Pages/AboutPages/Translators.xaml index 3b3fcbc4f0..921847d984 100644 --- a/src/UniGetUI/Pages/AboutPages/Translators.xaml +++ b/src/UniGetUI/Pages/AboutPages/Translators.xaml @@ -34,50 +34,52 @@ - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - + VerticalAlignment="Center" + Text="{x:Bind Language}" /> + + + + + + diff --git a/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml b/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml index 8ce295f2ee..5dc38397dd 100644 --- a/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml +++ b/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml @@ -133,7 +133,7 @@ - + @@ -152,6 +152,7 @@ diff --git a/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml.cs b/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml.cs index 373c9bf633..a85bb68c3d 100644 --- a/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml.cs +++ b/src/UniGetUI/Pages/DialogPages/DesktopShortcuts.xaml.cs @@ -152,6 +152,12 @@ public bool ExistsOnDisk get => File.Exists(Path); } + public string OpenShortcutLocationAutomationName => CoreTools.Translate("Open shortcut location"); + + public string RemoveShortcutFromListAutomationName => CoreTools.Translate("Remove shortcut from this list"); + + public string DeleteShortcutAutomationName => CoreTools.Translate("Delete shortcut {0}", Name); + public ShortcutEntry(string path, bool isDeletable) { Path = path; diff --git a/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml b/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml index 2aae4a6535..546cad9214 100644 --- a/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml +++ b/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml @@ -87,7 +87,7 @@ - + @@ -157,6 +157,7 @@ Width="32" Height="32" Padding="0" + AutomationProperties.Name="{x:Bind StopIgnoringUpdatesAutomationName, Mode=OneWay}" Click="{x:Bind RemoveFromIgnoredUpdates}"> diff --git a/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml.cs b/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml.cs index 65baf27936..7611987de0 100644 --- a/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml.cs +++ b/src/UniGetUI/Pages/DialogPages/IgnoredUpdates.xaml.cs @@ -91,6 +91,7 @@ public class IgnoredPackageEntry public string Version { get; } public string NewVersion { get; } public IPackageManager Manager { get; } + public string StopIgnoringUpdatesAutomationName => CoreTools.Translate("Stop ignoring updates for this package"); private ObservableCollection List { get; } public IgnoredPackageEntry(string id, string version, IPackageManager manager, ObservableCollection list) { diff --git a/src/UniGetUI/Pages/DialogPages/InstallOptions_Manager.xaml.cs b/src/UniGetUI/Pages/DialogPages/InstallOptions_Manager.xaml.cs index b662f1138e..b32918f37c 100644 --- a/src/UniGetUI/Pages/DialogPages/InstallOptions_Manager.xaml.cs +++ b/src/UniGetUI/Pages/DialogPages/InstallOptions_Manager.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using UniGetUI.Core.Language; using UniGetUI.Core.SettingsEngine.SecureSettings; @@ -23,11 +24,22 @@ public InstallOptions_Manager(IPackageManager manager) { Manager = manager; InitializeComponent(); - AdminCheckBox.Content = CoreTools.Translate("Run as admin"); - InteractiveCheckBox.Content = CoreTools.Translate("Interactive installation"); - HashCheckBox.Content = CoreTools.Translate("Skip hash check"); - UninstallPreviousVerOnUpdate.Content = CoreTools.Translate("Uninstall previous versions when updated"); - PreReleaseCheckBox.Content = CoreTools.Translate("Allow pre-release versions"); + string runAsAdminText = CoreTools.Translate("Run as admin"); + string interactiveInstallText = CoreTools.Translate("Interactive installation"); + string skipHashText = CoreTools.Translate("Skip hash check"); + string uninstallPreviousText = CoreTools.Translate("Uninstall previous versions when updated"); + string allowPreReleaseText = CoreTools.Translate("Allow pre-release versions"); + + AdminCheckBox.Content = runAsAdminText; + AutomationProperties.SetName(AdminCheckBox, runAsAdminText); + InteractiveCheckBox.Content = interactiveInstallText; + AutomationProperties.SetName(InteractiveCheckBox, interactiveInstallText); + HashCheckBox.Content = skipHashText; + AutomationProperties.SetName(HashCheckBox, skipHashText); + UninstallPreviousVerOnUpdate.Content = uninstallPreviousText; + AutomationProperties.SetName(UninstallPreviousVerOnUpdate, uninstallPreviousText); + PreReleaseCheckBox.Content = allowPreReleaseText; + AutomationProperties.SetName(PreReleaseCheckBox, allowPreReleaseText); ArchLabel.Text = CoreTools.Translate("Architecture to install:"); ScopeLabel.Text = CoreTools.Translate("Installation scope:"); LocationLabel.Text = CoreTools.Translate("Install location:"); diff --git a/src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml b/src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml index a6387a926e..8b030a655b 100644 --- a/src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml +++ b/src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml @@ -15,7 +15,9 @@ mc:Ignorable="d"> - + diff --git a/src/UniGetUI/Pages/DialogPages/OperationFailedDialog.xaml b/src/UniGetUI/Pages/DialogPages/OperationFailedDialog.xaml index ab0b24d192..bf2a7989f6 100644 --- a/src/UniGetUI/Pages/DialogPages/OperationFailedDialog.xaml +++ b/src/UniGetUI/Pages/DialogPages/OperationFailedDialog.xaml @@ -22,14 +22,19 @@ Grid.Row="0" TextWrapping="WrapWholeWords" /> + HorizontalScrollMode="Disabled" + IsTabStop="True" + AutomationProperties.Name="Command-line output"> diff --git a/src/UniGetUI/Pages/DialogPages/OperationLiveLogPage.xaml b/src/UniGetUI/Pages/DialogPages/OperationLiveLogPage.xaml index cfc5a25662..4b32f06dd8 100644 --- a/src/UniGetUI/Pages/DialogPages/OperationLiveLogPage.xaml +++ b/src/UniGetUI/Pages/DialogPages/OperationLiveLogPage.xaml @@ -22,12 +22,16 @@ Grid.Column="0" x:FieldModifier="public" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" - CornerRadius="6"> + CornerRadius="6" + IsTabStop="True" + AutomationProperties.Name="Operation live log output"> @@ -188,6 +189,7 @@ Height="40" Margin="1,0,0,0" Padding="0,0,2,0" + AutomationProperties.Name="{x:Bind MorePackageActionsAutomationName, Mode=OneWay}" CornerRadius="0,8,8,0" Style="{StaticResource AccentButtonStyle}"> diff --git a/src/UniGetUI/Pages/DialogPages/PackageDetailsPage.xaml.cs b/src/UniGetUI/Pages/DialogPages/PackageDetailsPage.xaml.cs index 73267e0277..aa5d61d8a1 100644 --- a/src/UniGetUI/Pages/DialogPages/PackageDetailsPage.xaml.cs +++ b/src/UniGetUI/Pages/DialogPages/PackageDetailsPage.xaml.cs @@ -31,6 +31,7 @@ public sealed partial class PackageDetailsPage : Page public IPackage? AvailablePackage; public IPackage? UpgradablePackage; public IPackage? InstalledPackage; + public string MorePackageActionsAutomationName => CoreTools.Translate("More package actions"); private readonly InstallOptionsPage InstallOptionsPage; public event EventHandler? Close; diff --git a/src/UniGetUI/Pages/HelpPage.xaml b/src/UniGetUI/Pages/HelpPage.xaml index 98894168a2..ca2f63beab 100644 --- a/src/UniGetUI/Pages/HelpPage.xaml +++ b/src/UniGetUI/Pages/HelpPage.xaml @@ -50,6 +50,7 @@ Width="35" Height="35" Padding="5" + AutomationProperties.Name="{x:Bind HelpBackAutomationName, Mode=OneWay}" Click="BackButton_Click" CornerRadius="4,0,0,4"> @@ -60,6 +61,7 @@ Width="35" Height="35" Padding="5" + AutomationProperties.Name="{x:Bind HelpForwardAutomationName, Mode=OneWay}" Click="RightButton_Click" CornerRadius="0"> @@ -70,6 +72,7 @@ Width="35" Height="35" Padding="5" + AutomationProperties.Name="{x:Bind HelpHomeAutomationName, Mode=OneWay}" Click="HomeButton_Click" CornerRadius="0"> @@ -80,6 +83,7 @@ Width="35" Height="35" Padding="5" + AutomationProperties.Name="{x:Bind HelpReloadAutomationName, Mode=OneWay}" Click="ReloadButton_Click" CornerRadius="0,4,4,0"> diff --git a/src/UniGetUI/Pages/HelpPage.xaml.cs b/src/UniGetUI/Pages/HelpPage.xaml.cs index dd05bfe1d7..8b6abd56a2 100644 --- a/src/UniGetUI/Pages/HelpPage.xaml.cs +++ b/src/UniGetUI/Pages/HelpPage.xaml.cs @@ -17,6 +17,10 @@ public sealed partial class HelpPage : Page, IDisposable, IEnterLeaveListener private bool Initialized; private WebView2? webView; private Uri? lastUri; + public string HelpBackAutomationName => CoreTools.Translate("Go back"); + public string HelpForwardAutomationName => CoreTools.Translate("Go forward"); + public string HelpHomeAutomationName => CoreTools.Translate("Go to help home"); + public string HelpReloadAutomationName => CoreTools.Translate("Reload page"); public HelpPage() { diff --git a/src/UniGetUI/Pages/MainView.xaml b/src/UniGetUI/Pages/MainView.xaml index 1e1262898a..737aaef718 100644 --- a/src/UniGetUI/Pages/MainView.xaml +++ b/src/UniGetUI/Pages/MainView.xaml @@ -95,6 +95,7 @@ Height="32" Margin="6,0,0,0" Padding="0" + AutomationProperties.Name="{x:Bind Tooltip, Mode=OneWay}" CornerRadius="6" ToolTipService.ToolTip="{x:Bind Tooltip}"> @@ -224,6 +225,7 @@ Padding="0" HorizontalAlignment="Stretch" VerticalAlignment="Center" + AutomationProperties.Name="{x:Bind OperationOptionsAutomationName, Mode=OneWay}" BorderThickness="0" CornerRadius="0,6,6,0" Flyout="{x:Bind OpMenu}" @@ -373,6 +375,7 @@ Height="12" Margin="1,0,1,0" Padding="0,0,0,0" + AutomationProperties.Name="{x:Bind ResizeOperationsAreaAutomationName, Mode=OneWay}" CornerRadius="4" Orientation="Horizontal" /> @@ -387,6 +390,7 @@ Grid.Column="1" Height="12" Padding="12,0,12,0" + AutomationProperties.Name="{x:Bind ExpandCollapseOperationsAutomationName, Mode=OneWay}" Click="ExpandCollapseOpList_Click" CornerRadius="4" Foreground="{ThemeResource ScrollBarButtonPointerOverBackgroundThemeBrush}"> @@ -397,6 +401,7 @@ Grid.Column="2" Height="12" Padding="12,0,12,0" + AutomationProperties.Name="{x:Bind OperationsListActionsAutomationName, Mode=OneWay}" Click="OperationSplitterMenuButton_Click" CornerRadius="4" Foreground="{ThemeResource ScrollBarButtonPointerOverBackgroundThemeBrush}"> @@ -408,6 +413,7 @@ Grid.Row="2" Grid.Column="0" x:FieldModifier="public" + AutomationProperties.Name="{x:Bind OperationsListAutomationName, Mode=OneWay}" ItemTemplate="{StaticResource OperationTemplate}" SelectionMode="None" SizeChanged="OperationScrollView_SizeChanged"> diff --git a/src/UniGetUI/Pages/MainView.xaml.cs b/src/UniGetUI/Pages/MainView.xaml.cs index 4543af9fab..894710e01e 100644 --- a/src/UniGetUI/Pages/MainView.xaml.cs +++ b/src/UniGetUI/Pages/MainView.xaml.cs @@ -57,6 +57,10 @@ public sealed partial class MainView : UserControl private PageType OldPage_t = PageType.Null; private PageType CurrentPage_t = PageType.Null; private List NavigationHistory = new(); + public string ExpandCollapseOperationsAutomationName => CoreTools.Translate("Expand or collapse operations list"); + public string OperationsListActionsAutomationName => CoreTools.Translate("Operation list actions"); + public string OperationsListAutomationName => CoreTools.Translate("Operations list"); + public string ResizeOperationsAreaAutomationName => CoreTools.Translate("Resize operations area"); AutoSuggestBox MainTextBlock; public event EventHandler? CanGoBackChanged; diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml index 9b4ef9bc6b..8b63c1705e 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml @@ -184,8 +184,14 @@ - - + + diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml.cs b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml.cs index db306fa41d..b853f1354f 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Backup.xaml.cs @@ -28,6 +28,9 @@ public sealed partial class Backup : Page, ISettingsPage private readonly GitHubBackupService _backupService; private bool _isLoggedIn; private bool _isLoading; + public string ResetBackupDirectoryAutomationName => CoreTools.Translate("Reset backup directory"); + public string OpenBackupDirectoryAutomationName => CoreTools.Translate("Open backup directory"); + public Backup() { this.InitializeComponent(); diff --git a/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml b/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml index 15708e7013..62a4616db4 100644 --- a/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml +++ b/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml @@ -57,6 +57,7 @@ Width="40" Height="40" Padding="6" + AutomationProperties.Name="{x:Bind BackAutomationName, Mode=OneWay}" VerticalAlignment="Center" Background="Transparent" BorderThickness="0"> diff --git a/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml.cs b/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml.cs index c1361c6af5..d23c14a271 100644 --- a/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/SettingsBasePage.xaml.cs @@ -1,12 +1,21 @@ using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Documents; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; using UniGetUI.Core.Tools; using Microsoft.UI.Xaml.Media.Animation; +using Microsoft.UI.Input; +using CommunityToolkit.WinUI.Controls; using UniGetUI.PackageEngine.ManagerClasses.Manager; using UniGetUI.Pages.SettingsPages.GeneralPages; using UniGetUI.Interface.Pages; using UniGetUI.PackageEngine.Interfaces; +using Windows.System; +using Windows.UI.Core; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -19,10 +28,15 @@ namespace UniGetUI.Pages.SettingsPages public sealed partial class SettingsBasePage : Page, IEnterLeaveListener, IInnerNavigationPage { bool IsManagers; + public string BackAutomationName => CoreTools.Translate("Back"); + public SettingsBasePage(bool isManagers) { IsManagers = isManagers; this.InitializeComponent(); + MainNavigationFrame.IsTabStop = false; + MainNavigationFrame.TabFocusNavigation = KeyboardNavigationMode.Local; + BackButton.PreviewKeyDown += BackButton_PreviewKeyDown; BackButton.Click += (_, _) => { if (MainNavigationFrame.Content is ManagersHomepage or SettingsHomepage) MainApp.Instance.MainWindow.GoBack(); @@ -65,6 +79,288 @@ private void MainNavigationFrame_Navigated(object sender, NavigationEventArgs e) page.NavigationRequested += Page_NavigationRequested; page.RestartRequired += Page_RestartRequired; if (page is PackageManagerPage pmpage) pmpage.ReapplyProperties += SettingsBasePage_ReapplyProperties; + + if (e.Content is FrameworkElement root) + { + DispatcherQueue.TryEnqueue(() => ApplyAccessibilityMetadata(root)); + } + } + + private static void ApplyAccessibilityMetadata(FrameworkElement root) + { + root.TabFocusNavigation = KeyboardNavigationMode.Local; + ApplyAccessibilityMetadataToNode(root); + } + + private static void ApplyAccessibilityMetadataToNode(DependencyObject node) + { + if (node is ScrollViewer scroller) + { + EnsureScrollViewerAccessibility(scroller); + } + + if (node is SettingsCard card) + { + EnsureSettingsCardAccessibility(card); + } + + if (node is ToggleSwitch toggle) + { + EnsureToggleSwitchAccessibility(toggle); + } + + if (node is ButtonBase button) + { + EnsureButtonAccessibility(button); + } + + int childCount = VisualTreeHelper.GetChildrenCount(node); + for (int i = 0; i < childCount; i++) + { + ApplyAccessibilityMetadataToNode(VisualTreeHelper.GetChild(node, i)); + } + } + + private static void EnsureScrollViewerAccessibility(ScrollViewer scroller) + { + scroller.IsTabStop = false; + scroller.TabFocusNavigation = KeyboardNavigationMode.Local; + } + + private void BackButton_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key != VirtualKey.Tab) + { + return; + } + + bool isShiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + if (isShiftPressed) + { + return; + } + + if (TryFocusMainContent()) + { + e.Handled = true; + } + } + + private bool TryFocusMainContent() + { + if (MainNavigationFrame.Content is not FrameworkElement root) + { + return false; + } + + FrameworkElement? target = FindFirstFocusableMainContentElement(root, allowHyperlinks: false) + ?? FindFirstFocusableMainContentElement(root, allowHyperlinks: true); + return target?.Focus(FocusState.Programmatic) ?? false; + } + + private static FrameworkElement? FindFirstFocusableMainContentElement(DependencyObject node, bool allowHyperlinks) + { + if (node is SettingsCard card && card.IsClickEnabled && IsFocusable(card, allowHyperlinks)) + { + return card; + } + + if (node is FrameworkElement element && IsFocusable(element, allowHyperlinks)) + { + return element; + } + + int childCount = VisualTreeHelper.GetChildrenCount(node); + for (int i = 0; i < childCount; i++) + { + FrameworkElement? candidate = FindFirstFocusableMainContentElement(VisualTreeHelper.GetChild(node, i), allowHyperlinks); + if (candidate is not null) + { + return candidate; + } + } + + return null; + } + + private static bool IsFocusable(FrameworkElement element, bool allowHyperlinks) + { + if (element is not (SettingsCard or ButtonBase or ToggleSwitch or ComboBox or TextBox or PasswordBox or CheckBox or RadioButton)) + { + return false; + } + + if (!allowHyperlinks && element is HyperlinkButton) + { + return false; + } + + if (element is not Control control) + { + return false; + } + + return control.IsEnabled && control.IsTabStop && control.Visibility == Visibility.Visible; + } + + private static void EnsureSettingsCardAccessibility(SettingsCard card) + { + string headerText = ExtractTextFromObject(card.Header); + string descriptionText = ExtractTextFromObject(card.Description); + string cardName = BuildCombinedText(headerText, descriptionText); + if (!string.IsNullOrWhiteSpace(cardName) && string.IsNullOrWhiteSpace(AutomationProperties.GetName(card))) + { + AutomationProperties.SetName(card, cardName); + } + + if (card.IsClickEnabled) + { + card.IsTabStop = true; + card.UseSystemFocusVisuals = true; + } + } + + private static void EnsureToggleSwitchAccessibility(ToggleSwitch toggle) + { + string currentName = (AutomationProperties.GetName(toggle) ?? string.Empty).Trim(); + if (!NeedsContextualName(currentName, toggle.OnContent, toggle.OffContent)) + { + return; + } + + string fallbackName = FindAncestorSettingsCardName(toggle); + if (!string.IsNullOrWhiteSpace(fallbackName)) + { + AutomationProperties.SetName(toggle, fallbackName); + } + } + + private static void EnsureButtonAccessibility(ButtonBase button) + { + string currentName = (AutomationProperties.GetName(button) ?? string.Empty).Trim(); + if (!NeedsContextualName(currentName)) + { + return; + } + + string name = ExtractTextFromObject(button.Content); + if (string.IsNullOrWhiteSpace(name)) + { + name = ExtractTextFromObject(ToolTipService.GetToolTip(button)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + name = FindAncestorSettingsCardName(button); + } + + if (!string.IsNullOrWhiteSpace(name)) + { + AutomationProperties.SetName(button, name); + } + } + + private static bool NeedsContextualName(string currentName, params object?[] alternatives) + { + if (string.IsNullOrWhiteSpace(currentName)) + { + return true; + } + + foreach (object? alternative in alternatives) + { + string alternativeText = ExtractTextFromObject(alternative); + if (!string.IsNullOrWhiteSpace(alternativeText) + && string.Equals(currentName, alternativeText, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (currentName.StartsWith("ms-resource://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + string translatedEnabled = CoreTools.Translate("Enabled"); + string translatedDisabled = CoreTools.Translate("Disabled"); + string[] genericNames = + [ + "button", + "on", + "off", + "enabled", + "disabled", + translatedEnabled, + translatedDisabled + ]; + + return genericNames + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Any(name => string.Equals(currentName, name.Trim(), StringComparison.OrdinalIgnoreCase)); + } + + private static string FindAncestorSettingsCardName(DependencyObject node) + { + for (DependencyObject? current = VisualTreeHelper.GetParent(node); current is not null; current = VisualTreeHelper.GetParent(current)) + { + if (current is not SettingsCard card) + { + continue; + } + + EnsureSettingsCardAccessibility(card); + string name = AutomationProperties.GetName(card); + if (!string.IsNullOrWhiteSpace(name)) + { + return name; + } + } + + return string.Empty; + } + + private static string ExtractTextFromObject(object? value) + { + return value switch + { + string s => s.Trim(), + TextBlock textBlock => textBlock.Text.Trim(), + Run run => run.Text.Trim(), + FrameworkElement element => ExtractTextFromElement(element), + _ => string.Empty + }; + } + + private static string ExtractTextFromElement(FrameworkElement element) + { + List parts = []; + CollectTextParts(element, parts); + return BuildCombinedText(parts.ToArray()); + } + + private static void CollectTextParts(DependencyObject node, List parts) + { + switch (node) + { + case TextBlock textBlock when !string.IsNullOrWhiteSpace(textBlock.Text): + parts.Add(textBlock.Text.Trim()); + break; + case Run run when !string.IsNullOrWhiteSpace(run.Text): + parts.Add(run.Text.Trim()); + break; + } + + int childCount = VisualTreeHelper.GetChildrenCount(node); + for (int i = 0; i < childCount; i++) + { + CollectTextParts(VisualTreeHelper.GetChild(node, i), parts); + } + } + + private static string BuildCombinedText(params string[] parts) + { + return string.Join(". ", parts.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p.Trim()).Distinct()); } private void SettingsBasePage_ReapplyProperties(object? sender, EventArgs e) diff --git a/src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml b/src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml index 0a04e5745b..c698f86013 100644 --- a/src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml +++ b/src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml @@ -55,6 +55,7 @@ @@ -273,6 +274,7 @@ Grid.Column="2" Margin="1,-4,0,0" HorizontalAlignment="Left" + AutomationProperties.Name="{x:Bind SelectPackageAutomationName, Mode=OneWay}" VerticalAlignment="Top" IsChecked="{x:Bind IsChecked, Mode=TwoWay}" /> @@ -282,6 +284,7 @@ Height="22" Padding="0" VerticalAlignment="Bottom" + AutomationProperties.Name="{x:Bind PackageOptionsAutomationName, Mode=OneWay}" Background="Transparent" BorderThickness="0" Click="{x:Bind RightClick}" @@ -396,6 +399,7 @@ Grid.Row="0" Margin="1,-4,0,0" HorizontalAlignment="Left" + AutomationProperties.Name="{x:Bind SelectPackageAutomationName, Mode=OneWay}" VerticalAlignment="Top" IsChecked="{x:Bind IsChecked, Mode=TwoWay}" /> @@ -1018,6 +1043,7 @@ Grid.ColumnSpan="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" + AutomationProperties.Name="{x:Bind SortByPackageNameAutomationName, Mode=OneWay}" BorderThickness="0" CornerRadius="0" />