.NET MAUI Accessibility. Створюємо доступні інтерфейси
Привіт, спільното! Мене звати Садовий Микола, я .NET розробник у компанії GlobalLogic. Працюю з екосистемою .NET понад 7 років, а з MAUI експериментую з перших preview. Протягом останнього часу мене особливо цікавить тема (A11y) у кросплатформених додатках.
Чому це важливо?
Доступність — це не лише про людей з порушеннями зору чи моторики. Це про загальну якість UX:
- підтримка голосових помічників (Siri, Google Assistant);
- коректна клавіатурна навігація;
- масштабування інтерфейсу на різних пристроях.
У цій статті ми:
- розглянемо вбудовані інструменти MAUI для A11y;
- створимо движок перевірки доступності (A11yAudit Engine);
- реалізуємо 3 приклади доступних UI-сторінок.
Accessibility у .NET MAUI. Основи
Спершу розглянемо, що вже доступно «з коробки»:
1) SemanticProperties.Description — додає голосовий опис елементу для скрінрідерів:
<Entry Placeholder="Email" SemanticProperties.Description="Поле для введення електронної пошти"/>
У VoiceOver чи TalkBack користувач почує: «Поле для введення електронної пошти».
2) SemanticProperties.Hint — дозволяє додати пояснення до дії (аналог aria-describedby у web):
<Button Text="Надіслати" SemanticProperties.Hint="Надішле форму реєстрації"/>
Користувач зрозуміє, що саме виконує кнопка.
3) SemanticProperties.HeadingLevel — використовується для логічної структури заголовків:
<Label Text="Реєстрація" SemanticProperties.HeadingLevel="Level1" />
Це допомагає скрінрідерам будувати ієрархію документа.
4) AutomationProperties.HelpText — додає додаткові підказки:
<CheckBox Content="Приймаю умови" AutomationProperties.HelpText="Це обов'язкове поле"/>
Сумісність з:
- iOS — VoiceOver;
- Android — TalkBack;
- Windows — Narrator.
Чого ще бракує:
- Візуального інспектора доступності (як у Android Studio або Xcode).
- Офіційного тулкіта для WCAG 2.1.
- Автоматичних попереджень про відсутність атрибутів.
Тому далі ми створимо власний A11yAudit Engine.
Реалізуємо перевірки доступності. Створення A11yAudit Engine
Мета — автоматично аналізувати UI-дерево та виводити попередження, якщо відсутні ключові атрибути доступності.
Що для цього маємо зробити — розглянемо кроки:
1) Створюємо бібліотеку A11ySample.Helpers
2) Архітектура — AccessibilityAuditor + IAccessibilityCheck
public interface IAccessibilityCheck
{
string Name { get; }
string Validate(VisualElement element);
}
[Flags]
public enum AccessibilityAuditMode
{
None = 0,
Description = 1 << 0,
Hint = 1 << 1,
Heading = 1 << 2,
All = Description | Hint | Heading
}
public static class AccessibilityAuditExtensions
{
public static void RunAccessibilityAudit(this Page page, AccessibilityAuditMode mode)
{
if (mode == AccessibilityAuditMode.None)
return;
if (page is IVisualTreeElement)
{
var auditor = new AccessibilityAuditor();
if (mode.HasFlag(AccessibilityAuditMode.Description))
auditor.Register(new DescriptionCheck());
if (mode.HasFlag(AccessibilityAuditMode.Hint))
auditor.Register(new HintCheck());
if (mode.HasFlag(AccessibilityAuditMode.Heading))
auditor.Register(new HeadingCheck());
var results = auditor.Audit(page);
foreach (var warning in results)
{
System.Diagnostics.Debug.WriteLine($"[A11yAudit] : {warning}");
}
}
}
}
internal class AccessibilityAuditor
{
private readonly List<IAccessibilityCheck> _checks = new();
public void Register(IAccessibilityCheck check) => _checks.Add(check);
public IEnumerable<string> Audit(Page root)
{
if (root == null) yield break;
foreach (var result in AuditElement(root))
yield return result;
}
private IEnumerable<string> AuditElement(Element element)
{
if (element is VisualElement ve)
{
foreach (var check in _checks)
{
var result = check.Validate(ve);
if (!string.IsNullOrEmpty(result))
yield return result;
}
}
if (element is IElementController controller)
{
foreach (var child in controller.LogicalChildren)
{
if (child is ShellContent sc && sc.ContentTemplate != null)
{
var page = (Page)sc.ContentTemplate.CreateContent();
foreach (var result in AuditElement(page))
yield return result;
(page as IDisposable)?.Dispose();
}
else
{
foreach (var result in AuditElement(child))
yield return result;
}
}
}
}
}
3) Виконуємо основні перевірки:
internal class HintCheck : IAccessibilityCheck
{
public string Name => "Hint";
public string Validate(IVisualTreeElement element)
{
if (element is Button btn)
{
var hint = SemanticProperties.GetHint(btn);
if (string.IsNullOrWhiteSpace(hint))
return $"Button '{btn.Text}' doesn't have Hint";
}
return null;
}
}
internal class HeadingCheck : IAccessibilityCheck
{
public string Name => "Heading";
public string Validate(IVisualTreeElement element)
{
if (element is Label lbl && SemanticProperties.GetHeadingLevel(lbl) == SemanticHeadingLevel.None)
return $"Label '{lbl.Text}' has no header level";
return null;
}
}
internal class DescriptionCheck : IAccessibilityCheck
{
public string Name => "Description";
public string Validate(IVisualTreeElement element)
{
if (element is VisualElement ve)
{
var description = SemanticProperties.GetDescription(ve);
if (string.IsNullOrWhiteSpace(description))
return $"{ve.GetType().Name} doesn't have SemanticProperties.Description";
}
return null;
}
}
Використання:
public App()
{
InitializeComponent();
MainPage = new AppShell();
#if DEBUG
var shell = new AppShell();
shell.RunAccessibilityAudit(AccessibilityAuditMode.All);
#endif
}
Демонстраційні приклади. 3 сторінки — 3 аспекти доступності
Перший приклад: Semantic UI for Screen Readers.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="A11ySample.Example1Page" Title="Example 1"> <VerticalStackLayout Padding="20" Spacing="16"> <Label Text="User Profile" FontSize="32" SemanticProperties.HeadingLevel="Level1" SemanticProperties.Description="Main heading for the user profile section" /> <Label Text="Username" SemanticProperties.Description="Field label for entering username" /> <Entry Placeholder="Enter username" SemanticProperties.Description="Input field for username" /> <Label Text="Password" SemanticProperties.Description="Field label for entering password" /> <Entry Placeholder="Enter password" IsPassword="True" SemanticProperties.Description="Input field for password" /> <Button Text="Login"/> </VerticalStackLayout> </ContentPage>
Другий приклад: Keyboard Focus & Navigation.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="A11ySample.Example2Page"
Title="Example 2">
<VerticalStackLayout Padding="20" Spacing="16">
<Label
Text="Keyboard Navigation"
FontSize="28"
SemanticProperties.HeadingLevel="Level1" />
<Entry
x:Name="FirstInput"
Placeholder="First field"
ZIndex="0" />
<Entry
x:Name="SecondInput"
Placeholder="Second field"
ZIndex="1" />
<Button
Text="Focus First Field"
ZIndex="2"
Clicked="OnFocusFirstClicked" />
</VerticalStackLayout>
</ContentPage>
private void OnFocusFirstClicked(object sender, EventArgs e)
{
FirstInput.Focus();
}
Третій приклад: Visual Accessibility.
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="A11ySample.Example3Page" Title="Example 3"> <VerticalStackLayout Padding="20" Spacing="20"> <Label Text="High Contrast Mode" FontSize="28" TextColor="Black" BackgroundColor="Yellow" SemanticProperties.HeadingLevel="Level1" /> <Label Text="This screen uses strong color contrast and large font sizes to improve readability." FontSize="20" TextColor="Black" /> <BoxView HeightRequest="2" Color="Black" /> <Label Text="Tip: Try enabling High Contrast mode in your OS settings and zooming your text." FontSize="18" TextColor="DarkBlue" /> </VerticalStackLayout> </ContentPage>
Як ми бачимо, вбудований Accessibility Checker не знаходить допущених помилок.
У Visual Studio Accessibility Checker інтегрований здебільшого для WinUI/WPF/UWP. Для MAUI підтримка ще дуже обмежена: він може не ловити відсутність SemanticProperties і бачити лише базові речі (тип кнопки, текст).

Висновки
У процесі ми розібралися, що вбудовані засоби доступності у Visual Studio (Accessibility Checker у Live Visual Tree) для .NET MAUI поки що не дають повної картини. Вони працюють поверх UI Automation API конкретної платформи (WinUI, Android, iOS), і часто вважають елемент «доступним» навіть без спеціальних SemanticProperties. Наприклад, Label.Text інтерпретується як опис автоматично, а кнопка без Hint вважається валідною, бо має лише Text. Тому такий інструмент каже «No issues found», хоча з точки зору WCAG це не зовсім коректно.
Натомість наш кастомний AccessibilityAuditor вийшов більш строгим і практичним. Він перевіряє саме MAUI-рівень — тобто наявність SemanticProperties.Description, SemanticProperties.Hint, SemanticProperties.HeadingLevel. Це дозволяє виявляти ті проблеми, які реальний скрінрідер (VoiceOver, TalkBack, Narrator) потім би оголосив неправильно або неповно. Ми зробили його рекурсивним, щоб він обходив усе дерево елементів (включно з вкладеними Layout і ContentView), а також навчили запускати його через зручний extension, який викликається лише у Development-режимі. Це дає можливість інтегрувати аудит у CI або навіть у runtime-логіку для внутрішнього тестування.
Крім того, ми знайшли спосіб запускати аудит офлайн — тобто створювати сторінки з DataTemplate або Shell-контенту, проганяти перевірки і одразу їх звільняти, без показу на екрані. Це відкриває можливість написати unit-тести чи інтеграційні перевірки доступності, які проганятимуть усі сторінки застосунку ще на етапі розробки. Таким чином можна гарантувати, що жодна кнопка чи поле вводу не піде у реліз без необхідних SemanticProperties.
У результаті ми отримали повноцінний A11yAudit Engine для .NET MAUI: він гнучкий (керується через enum-флаги), строгий (виявляє реальні проблеми на рівні SemanticProperties), і зручний у використанні (extension-метод для сторінок, можливість запуску в OnAppearing, Loaded або навіть у тестах). Це рішення можна розвивати далі — наприклад, додати перевірку контрасту кольорів, розміру шрифту або навіть валідацію проти правил WCAG.
Ресурси
- Microsoft Learn — Accessibility in .NET MAUI: learn.microsoft.com/...ndamentals/accessibility
- WCAG 2.1 Guidelines: www.w3.org/TR/WCAG21
- Inclusive Design Toolkit: inclusivedesigntoolkit.com
- github.com/...1ty-MAUI-Helper/tree/main

Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів