Достъпност в .NET MAUI: SemanticProperties, екранни четци и WCAG 2.2 съвместимост

Практическо ръководство за изграждане на достъпни .NET MAUI приложения — от SemanticProperties и SemanticScreenReader до WCAG 2.2 съвместимост, цветови контраст и тестване с TalkBack, VoiceOver и Narrator.

.NET MAUI Достъпност: WCAG 2.2 Guide 2026

Въведение: Защо достъпността наистина има значение

Достъпността (accessibility) в мобилните приложения не е просто хубава добавка — тя е фундаментално изискване за създаването на софтуер, който реално обслужва всички потребители. Ако не сте се замисляли сериозно за това досега, ето един факт: според данни на Световната здравна организация, над 1.3 милиарда души по света живеят с някаква форма на увреждане. Това означава, че приблизително 16% от световното население може да има затруднения с приложения, които не са проектирани с мисъл за достъпност.

Честно казано, много от нас (включително и аз) в началото пренебрегваме тази тема — защото „работи за повечето хора". Но точно тук е капанът.

В контекста на .NET MAUI (Multi-platform App UI), Microsoft предоставя богат набор от инструменти и API-та, с които можем да създаваме достъпни приложения за Android, iOS, Windows и macOS от една обща кодова база. В това ръководство ще разгледаме подробно всички аспекти на достъпността в .NET MAUI — от семантичните свойства и екранните четци до цветовия контраст и тестването.

Освен моралния императив, съществуват и законови изисквания за достъпност на софтуера. В Европейския съюз Европейският акт за достъпност (European Accessibility Act) изисква дигиталните продукти и услуги да бъдат достъпни до юни 2025 г. В САЩ Americans with Disabilities Act (ADA) и Section 508 налагат подобни стандарти. Неспазването може да доведе до съдебни дела и значителни глоби — нещо, което никой от нас не иска.

Ползите от достъпния дизайн обаче надхвърлят правните задължения:

  • По-голяма потребителска база — достъпните приложения достигат до повече хора
  • По-добро потребителско изживяване за всички — подобренията в достъпността често подобряват UX за всички потребители
  • По-високо качество на кода — семантичната структура води до по-поддържаем код
  • SEO предимства — за уеб-базирани компоненти семантичната структура подобрява индексирането
  • Конкурентно предимство — достъпните приложения се отличават на пазара

WCAG 2.2: Стандартът за достъпност и мобилните приложения

Web Content Accessibility Guidelines (WCAG) 2.2 е международният стандарт за достъпност на дигиталното съдържание, публикуван от W3C. Въпреки че е създаден първоначално за уеб, принципите му се прилагат и за мобилни приложения. WCAG 2.2 е организиран около четири основни принципа, известни като POUR:

  1. Perceivable (Възприемаем) — информацията и компонентите на потребителския интерфейс трябва да бъдат представени по начин, който потребителите могат да възприемат
  2. Operable (Управляем) — компонентите на интерфейса и навигацията трябва да бъдат управляеми
  3. Understandable (Разбираем) — информацията и работата с интерфейса трябва да бъдат разбираеми
  4. Robust (Устойчив) — съдържанието трябва да бъде достатъчно устойчиво, за да се интерпретира надеждно от помощни технологии

WCAG дефинира три нива на съответствие:

  • Ниво A — минимално ниво на достъпност
  • Ниво AA — препоръчителното ниво (повечето законодателства изискват точно него)
  • Ниво AAA — най-високото ниво на достъпност

За мобилните приложения WCAG 2.2 въвежда няколко нови критерия, които са особено важни за нас:

  • 2.5.7 Dragging Movements (Ниво AA) — функционалност, изискваща плъзгане, трябва да има алтернативен начин за управление
  • 2.5.8 Target Size (Minimum) (Ниво AA) — целевите области за докосване трябва да бъдат поне 24x24 CSS пиксела
  • 3.3.7 Redundant Entry (Ниво A) — информация, вече въведена от потребителя, не трябва да се изисква повторно
  • 3.3.8 Accessible Authentication (Minimum) (Ниво AA) — процесът на автентикация не трябва да разчита на когнитивни тестове

SemanticProperties в .NET MAUI

.NET MAUI предоставя SemanticProperties като основен механизъм за добавяне на семантична информация към визуалните елементи. Тези свойства позволяват на помощните технологии (като екранни четци) да разберат предназначението и контекста на всеки елемент от интерфейса.

Нека разгледаме отделните свойства едно по едно.

SemanticProperties.Description

Свойството Description предоставя текстово описание, което екранните четци прочитат на глас. Това е еквивалент на alt атрибута за изображения в HTML или contentDescription в Android.

<!-- XAML пример: Описание на изображение -->
<Image Source="profile_photo.png"
       SemanticProperties.Description="Профилна снимка на потребителя Иван Петров"
       WidthRequest="80"
       HeightRequest="80" />

<!-- XAML пример: Описание на бутон с икона -->
<ImageButton Source="delete_icon.png"
             SemanticProperties.Description="Изтриване на избрания елемент"
             Command="{Binding DeleteCommand}"
             WidthRequest="44"
             HeightRequest="44" />

<!-- XAML пример: Описание на персонализиран контрол -->
<Frame SemanticProperties.Description="Карта с информация за поръчка номер 12345, статус: изпратена"
       Padding="16"
       CornerRadius="8">
    <VerticalStackLayout>
        <Label Text="Поръчка #12345" />
        <Label Text="Статус: Изпратена" />
    </VerticalStackLayout>
</Frame>

Същото може да се постигне програмно в C#:

// C# пример: Задаване на семантично описание
var profileImage = new Image
{
    Source = "profile_photo.png",
    WidthRequest = 80,
    HeightRequest = 80
};
SemanticProperties.SetDescription(profileImage,
    "Профилна снимка на потребителя Иван Петров");

// Динамично обновяване на описанието
var statusIcon = new Image { Source = "status_icon.png" };
SemanticProperties.SetDescription(statusIcon,
    $"Статус на връзката: {(isConnected ? "Свързан" : "Прекъснат")}");

SemanticProperties.Hint

Свойството Hint дава допълнителен контекст за това какво ще се случи, когато потребителят взаимодейства с елемента. Подсказката се чете след описанието и помага на потребителя да разбере какъв ще бъде резултатът от действието.

<!-- XAML примери за Hint -->
<Button Text="Добави в количка"
        SemanticProperties.Description="Добави в количка"
        SemanticProperties.Hint="Добавя текущия продукт към вашата кошница за пазаруване"
        Command="{Binding AddToCartCommand}" />

<Switch IsToggled="{Binding IsDarkMode}"
        SemanticProperties.Description="Тъмен режим"
        SemanticProperties.Hint="Превключва между светъл и тъмен режим на приложението" />

<Slider Minimum="0" Maximum="100"
        Value="{Binding Volume}"
        SemanticProperties.Description="Сила на звука"
        SemanticProperties.Hint="Плъзнете за да регулирате силата на звука от 0 до 100 процента" />

SemanticProperties.HeadingLevel

Свойството HeadingLevel маркира елемент като заглавие с определено ниво (от 1 до 9). Това позволява на потребителите на екранни четци да навигират бързо между секциите, подобно на HTML заглавията (h1-h6). Може да звучи като дребна подробност, но от собствен опит мога да кажа — прави огромна разлика за хората, които използват VoiceOver ротора.

<!-- XAML пример: Йерархия от заглавия -->
<VerticalStackLayout Padding="16" Spacing="12">

    <Label Text="Настройки на профила"
           FontSize="28"
           FontAttributes="Bold"
           SemanticProperties.HeadingLevel="Level1" />

    <Label Text="Лична информация"
           FontSize="22"
           FontAttributes="Bold"
           SemanticProperties.HeadingLevel="Level2" />

    <Entry Placeholder="Име"
           Text="{Binding FirstName}"
           SemanticProperties.Description="Поле за въвеждане на име" />

    <Entry Placeholder="Фамилия"
           Text="{Binding LastName}"
           SemanticProperties.Description="Поле за въвеждане на фамилия" />

    <Label Text="Настройки за известия"
           FontSize="22"
           FontAttributes="Bold"
           SemanticProperties.HeadingLevel="Level2" />

    <Label Text="Имейл известия"
           FontSize="18"
           FontAttributes="Bold"
           SemanticProperties.HeadingLevel="Level3" />

    <Switch IsToggled="{Binding EmailNotifications}"
            SemanticProperties.Description="Имейл известия"
            SemanticProperties.Hint="Включва или изключва получаването на имейл известия" />

</VerticalStackLayout>

Задаване на ниво на заглавие от C# код:

// C# пример: Задаване на HeadingLevel програмно
var pageTitle = new Label
{
    Text = "Настройки на профила",
    FontSize = 28,
    FontAttributes = FontAttributes.Bold
};
SemanticProperties.SetHeadingLevel(pageTitle, SemanticHeadingLevel.Level1);

var sectionTitle = new Label
{
    Text = "Лична информация",
    FontSize = 22,
    FontAttributes = FontAttributes.Bold
};
SemanticProperties.SetHeadingLevel(sectionTitle, SemanticHeadingLevel.Level2);

SemanticScreenReader: Програмни обявления

Класът SemanticScreenReader позволява на приложението да инструктира екранния четец да прочете съобщение на глас. Това е изключително полезно за динамични промени в съдържанието, за които потребителят трябва да разбере, без да фокусира конкретен елемент.

Основна употреба на Announce()

// Основно обявление
SemanticScreenReader.Default.Announce("Продуктът беше добавен в количката.");

// Обявление при грешка при валидация
private void OnFormSubmit()
{
    var errors = ValidateForm();
    if (errors.Any())
    {
        string errorMessage = $"Формулярът съдържа {errors.Count} грешки. " +
                              string.Join(". ", errors);
        SemanticScreenReader.Default.Announce(errorMessage);
        return;
    }

    SemanticScreenReader.Default.Announce("Формулярът беше изпратен успешно.");
}

// Обявление при зареждане на данни
private async Task LoadDataAsync()
{
    SemanticScreenReader.Default.Announce("Зареждане на данните, моля изчакайте.");

    try
    {
        var data = await _dataService.GetItemsAsync();
        Items = new ObservableCollection<Item>(data);
        SemanticScreenReader.Default.Announce(
            $"Заредени са {data.Count} елемента.");
    }
    catch (Exception ex)
    {
        SemanticScreenReader.Default.Announce(
            "Грешка при зареждане на данните. Моля, опитайте отново.");
    }
}

Реален сценарий: достъпна кошница за пазаруване

Ето реалистичен пример с пълна реализация на достъпна страница с кошница:

public class ShoppingCartViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<CartItem> _items;

    [ObservableProperty]
    private decimal _totalPrice;

    [RelayCommand]
    private void RemoveItem(CartItem item)
    {
        Items.Remove(item);
        TotalPrice = Items.Sum(i => i.Price * i.Quantity);

        SemanticScreenReader.Default.Announce(
            $"{item.Name} беше премахнат от количката. " +
            $"Обща сума: {TotalPrice:C}. " +
            $"Оставащи продукти: {Items.Count}.");
    }

    [RelayCommand]
    private void UpdateQuantity(CartItem item)
    {
        TotalPrice = Items.Sum(i => i.Price * i.Quantity);

        SemanticScreenReader.Default.Announce(
            $"Количеството на {item.Name} беше променено на {item.Quantity}. " +
            $"Нова обща сума: {TotalPrice:C}.");
    }

    [RelayCommand]
    private async Task CheckoutAsync()
    {
        SemanticScreenReader.Default.Announce(
            "Обработка на поръчката, моля изчакайте.");

        bool success = await _orderService.PlaceOrderAsync(Items);

        if (success)
        {
            SemanticScreenReader.Default.Announce(
                "Поръчката ви беше направена успешно! " +
                "Ще получите потвърждение по имейл.");
        }
        else
        {
            SemanticScreenReader.Default.Announce(
                "Възникна проблем при обработката на поръчката. " +
                "Моля, проверете данните си и опитайте отново.");
        }
    }
}

AutomationProperties: Фин контрол над достъпността

AutomationProperties предоставят допълнителен контрол върху начина, по който помощните технологии взаимодействат с елементите на интерфейса. Двете най-важни свойства тук са IsInAccessibleTree и ExcludedWithChildren.

AutomationProperties.IsInAccessibleTree

Това свойство определя дали даден елемент е видим за помощните технологии. По подразбиране повечето интерактивни елементи са в дървото за достъпност. Задаването на false скрива елемента от екранните четци — перфектно за чисто декоративни неща.

<!-- Скриване на декоративни елементи от екранните четци -->
<Image Source="decorative_divider.png"
       AutomationProperties.IsInAccessibleTree="False"
       HeightRequest="2" />

<!-- Скриване на декоративна икона, когато Label вече съдържа информацията -->
<HorizontalStackLayout Spacing="8">
    <Image Source="email_icon.png"
           AutomationProperties.IsInAccessibleTree="False"
           WidthRequest="20"
           HeightRequest="20" />
    <Label Text="[email protected]"
           SemanticProperties.Description="Имейл адрес: [email protected]" />
</HorizontalStackLayout>

<!-- Скриване на анимация, която не носи информация -->
<Lottie:AnimationView
    Source="loading_animation.json"
    AutomationProperties.IsInAccessibleTree="False"
    RepeatCount="-1" />

AutomationProperties.ExcludedWithChildren

Свойството ExcludedWithChildren скрива елемента и всичките му дъщерни елементи от дървото за достъпност. Много удобно е за контейнери, обвиващи куп декоративни елементи.

<!-- Скриване на цял декоративен контейнер -->
<Grid AutomationProperties.ExcludedWithChildren="True">
    <Image Source="background_pattern.png" Aspect="AspectFill" />
    <BoxView Color="#80000000" />
    <Image Source="decorative_overlay.png" Opacity="0.3" />
</Grid>

<!-- Групиране на елементи за по-добра достъпност -->
<!-- Вместо да четем всеки елемент поотделно, четем групата -->
<Frame SemanticProperties.Description="Продукт: Безжични слушалки, цена 89.99 лв, налични 15 броя"
       Padding="12">
    <Grid AutomationProperties.ExcludedWithChildren="True"
          ColumnDefinitions="80, *"
          RowDefinitions="Auto, Auto, Auto">
        <Image Source="headphones.png" Grid.RowSpan="3" />
        <Label Text="Безжични слушалки" Grid.Column="1" />
        <Label Text="89.99 лв" Grid.Column="1" Grid.Row="1" />
        <Label Text="Налични: 15 бр." Grid.Column="1" Grid.Row="2" />
    </Grid>
</Frame>

Програмно задаване в C#:

// C# пример: Управление на AutomationProperties
var decorativeImage = new Image { Source = "pattern.png" };
AutomationProperties.SetIsInAccessibleTree(decorativeImage, false);

var decorativeContainer = new Grid();
decorativeContainer.Children.Add(new BoxView { Color = Colors.LightGray });
decorativeContainer.Children.Add(new Image { Source = "overlay.png" });
AutomationProperties.SetExcludedWithChildren(decorativeContainer, true);

// Динамично превключване на видимостта в дървото за достъпност
public void ToggleAccessibility(View element, bool isVisible)
{
    AutomationProperties.SetIsInAccessibleTree(element, isVisible);
}

Мащабиране на шрифтове с FontAutoScalingEnabled

Много потребители с намалено зрение увеличават размера на шрифта в системните настройки на устройството. .NET MAUI поддържа автоматично мащабиране на шрифтовете чрез свойството FontAutoScalingEnabled, което е включено по подразбиране за повечето контроли с текст.

Звучи тривиално, но може да счупи оформлението ви ако не сте внимателни.

Как работи FontAutoScalingEnabled

Когато потребителят промени размера на шрифта в системните настройки, .NET MAUI автоматично мащабира текста. Свойството FontAutoScalingEnabled контролира дали даден елемент ще участва в това мащабиране.

<!-- FontAutoScalingEnabled е true по подразбиране -->
<Label Text="Този текст ще се мащабира автоматично"
       FontSize="16" />

<!-- Изключване на автоматичното мащабиране за специфичен елемент -->
<Label Text="Този текст няма да се мащабира"
       FontSize="16"
       FontAutoScalingEnabled="False" />

<!-- Типичен сценарий: Заглавие на навигационна лента с фиксиран размер -->
<Label Text="Начало"
       FontSize="18"
       FontAutoScalingEnabled="False"
       HorizontalOptions="Center" />

<!-- Съдържание, което трябва да се мащабира -->
<Label Text="Добре дошли в нашето приложение. Тук ще намерите
             всичко необходимо за вашите покупки."
       FontSize="16"
       FontAutoScalingEnabled="True"
       LineBreakMode="WordWrap" />

Добри практики за мащабиране на шрифтове

За да гарантирате, че приложението ви работи добре при увеличен шрифт, следвайте тези препоръки:

<!-- Използвайте гъвкави оформления, които се адаптират -->
<ScrollView>
    <VerticalStackLayout Padding="16" Spacing="12">

        <!-- Използвайте MaxLines и LineBreakMode за контрол -->
        <Label Text="{Binding ProductName}"
               FontSize="20"
               FontAttributes="Bold"
               MaxLines="2"
               LineBreakMode="TailTruncation" />

        <!-- Описанието може да се разшири -->
        <Label Text="{Binding ProductDescription}"
               FontSize="16"
               LineBreakMode="WordWrap" />

        <!-- Бутон с адаптивен текст -->
        <Button Text="Добави в количката"
                FontSize="16"
                Padding="16,12"
                LineBreakMode="TailTruncation" />

    </VerticalStackLayout>
</ScrollView>

За тестване на мащабирането на шрифтовете:

  • Android: Настройки → Достъпност → Размер на шрифта (или Размер на показване)
  • iOS: Настройки → Достъпност → Размер на текста → По-голям текст
  • Windows: Настройки → Достъпност → Размер на текста

Тествайте приложението при максимален размер на шрифта, за да се уверите, че оформлението не се чупи и текстът не се отрязва. Вярвайте ми — ще откриете изненади.

Размер на целевите области за докосване

Според WCAG 2.2, критерий 2.5.8 (Target Size - Minimum), интерактивните елементи трябва да имат минимален размер от 24x24 CSS пиксела. Препоръчителният размер е значително по-голям — 44x44 точки за iOS (Apple Human Interface Guidelines) и 48x48 dp за Android (Material Design Guidelines).

Защо размерът има значение

Малките целеви области създават проблеми за множество групи потребители: хора с моторни увреждания, възрастни хора с намалена прецизност, потребители в движение, и хора с тремор. Дори и за хора без увреждания — опитайте да натиснете бутон от 16x16 пиксела в претъпкан автобус.

<!-- ЛОШО: Твърде малък бутон -->
<ImageButton Source="close_icon.png"
             WidthRequest="16"
             HeightRequest="16" />

<!-- ДОБРО: Минимален размер 24x24 (WCAG 2.2 AA) -->
<ImageButton Source="close_icon.png"
             WidthRequest="24"
             HeightRequest="24"
             Padding="4"
             SemanticProperties.Description="Затвори" />

<!-- НАЙ-ДОБРО: Препоръчителен размер 48x48 -->
<ImageButton Source="close_icon.png"
             WidthRequest="48"
             HeightRequest="48"
             Padding="12"
             SemanticProperties.Description="Затвори диалоговия прозорец" />

<!-- Увеличаване на целевата област без промяна на визуалния размер -->
<Grid WidthRequest="48" HeightRequest="48">
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding CloseCommand}" />
    </Grid.GestureRecognizers>
    <Image Source="close_icon.png"
           WidthRequest="20"
           HeightRequest="20"
           HorizontalOptions="Center"
           VerticalOptions="Center"
           AutomationProperties.IsInAccessibleTree="False" />
</Grid>

Стилове за осигуряване на консистентен размер

<!-- Дефиниране на глобални стилове за минимален размер -->
<ResourceDictionary>
    <Style TargetType="Button">
        <Setter Property="MinimumHeightRequest" Value="48" />
        <Setter Property="MinimumWidthRequest" Value="48" />
        <Setter Property="Padding" Value="16,12" />
    </Style>

    <Style TargetType="ImageButton">
        <Setter Property="MinimumHeightRequest" Value="48" />
        <Setter Property="MinimumWidthRequest" Value="48" />
        <Setter Property="Padding" Value="12" />
    </Style>

    <Style x:Key="AccessibleCheckBox" TargetType="CheckBox">
        <Setter Property="MinimumHeightRequest" Value="48" />
        <Setter Property="MinimumWidthRequest" Value="48" />
    </Style>
</ResourceDictionary>

Изисквания за цветови контраст

Цветовият контраст е един от най-важните (и най-често пренебрегваните) аспекти на визуалната достъпност. WCAG 2.2 дефинира конкретни изисквания за съотношението на контраста:

  • 4.5:1 — минимално съотношение за нормален текст (под 18pt или 14pt bold)
  • 3:1 — минимално съотношение за голям текст (18pt+ или 14pt+ bold) и UI компоненти
  • 3:1 — минимално съотношение за графични елементи и компоненти на интерфейса

Имплементация в .NET MAUI

<!-- ЛОШО: Нисък контраст - сив текст на бял фон -->
<Label Text="Трудно четим текст"
       TextColor="#999999"
       BackgroundColor="#FFFFFF" />
<!-- Съотношение на контраста: 2.85:1 - НЕ отговаря на WCAG AA -->

<!-- ДОБРО: Достатъчен контраст -->
<Label Text="Лесно четим текст"
       TextColor="#595959"
       BackgroundColor="#FFFFFF" />
<!-- Съотношение на контраста: 7.0:1 - отговаря на WCAG AAA -->

<!-- ДОБРО: Тъмна тема с адекватен контраст -->
<Label Text="Текст в тъмна тема"
       TextColor="#E0E0E0"
       BackgroundColor="#121212" />
<!-- Съотношение на контраста: 13.9:1 - отговаря на WCAG AAA -->

Достъпни цветови теми

<!-- Ресурсна дефиниция за достъпни цветове -->
<ResourceDictionary>
    <!-- Светла тема -->
    <Color x:Key="PrimaryTextLight">#1A1A1A</Color>
    <Color x:Key="SecondaryTextLight">#4A4A4A</Color>
    <Color x:Key="BackgroundLight">#FFFFFF</Color>
    <Color x:Key="SurfaceLight">#F5F5F5</Color>
    <Color x:Key="PrimaryLight">#0056B3</Color>
    <Color x:Key="ErrorLight">#C62828</Color>

    <!-- Тъмна тема -->
    <Color x:Key="PrimaryTextDark">#ECECEC</Color>
    <Color x:Key="SecondaryTextDark">#B0B0B0</Color>
    <Color x:Key="BackgroundDark">#121212</Color>
    <Color x:Key="SurfaceDark">#1E1E1E</Color>
    <Color x:Key="PrimaryDark">#64B5F6</Color>
    <Color x:Key="ErrorDark">#EF5350</Color>
</ResourceDictionary>

Помощен клас за проверка на контраста програмно:

/// <summary>
/// Помощен клас за изчисляване и проверка на цветови контраст
/// съгласно WCAG 2.2 стандарта.
/// </summary>
public static class ContrastChecker
{
    /// <summary>
    /// Изчислява относителната осветеност на цвят по WCAG формулата.
    /// </summary>
    public static double GetRelativeLuminance(Color color)
    {
        double r = LinearizeChannel(color.Red);
        double g = LinearizeChannel(color.Green);
        double b = LinearizeChannel(color.Blue);

        return 0.2126 * r + 0.7152 * g + 0.0722 * b;
    }

    private static double LinearizeChannel(float channel)
    {
        return channel <= 0.03928
            ? channel / 12.92
            : Math.Pow((channel + 0.055) / 1.055, 2.4);
    }

    /// <summary>
    /// Изчислява съотношението на контраста между два цвята.
    /// </summary>
    public static double GetContrastRatio(Color foreground, Color background)
    {
        double lum1 = GetRelativeLuminance(foreground);
        double lum2 = GetRelativeLuminance(background);

        double lighter = Math.Max(lum1, lum2);
        double darker = Math.Min(lum1, lum2);

        return (lighter + 0.05) / (darker + 0.05);
    }

    /// <summary>
    /// Проверява дали контрастът отговаря на WCAG AA за нормален текст (4.5:1).
    /// </summary>
    public static bool MeetsWcagAA(Color foreground, Color background)
    {
        return GetContrastRatio(foreground, background) >= 4.5;
    }

    /// <summary>
    /// Проверява дали контрастът отговаря на WCAG AA за голям текст (3:1).
    /// </summary>
    public static bool MeetsWcagAALargeText(Color foreground, Color background)
    {
        return GetContrastRatio(foreground, background) >= 3.0;
    }

    /// <summary>
    /// Проверява дали контрастът отговаря на WCAG AAA за нормален текст (7:1).
    /// </summary>
    public static bool MeetsWcagAAA(Color foreground, Color background)
    {
        return GetContrastRatio(foreground, background) >= 7.0;
    }
}

Важно е да не разчитате само на цвят за предаване на информация. Например, при валидация на форма не показвайте само червена рамка — добавете и текстово съобщение, и икона:

<!-- ЛОШО: Само цвят показва грешка -->
<Entry Text="{Binding Email}"
       BackgroundColor="{Binding HasError, Converter={StaticResource BoolToColorConverter}}" />

<!-- ДОБРО: Цвят + текст + икона -->
<VerticalStackLayout Spacing="4">
    <Entry Text="{Binding Email}"
           SemanticProperties.Description="Имейл адрес"
           SemanticProperties.Hint="Въведете вашия имейл адрес" />
    <HorizontalStackLayout Spacing="4"
                           IsVisible="{Binding HasEmailError}">
        <Image Source="error_icon.png"
               WidthRequest="16" HeightRequest="16"
               AutomationProperties.IsInAccessibleTree="False" />
        <Label Text="{Binding EmailErrorMessage}"
               TextColor="{StaticResource ErrorLight}"
               FontSize="12"
               SemanticProperties.Description="{Binding EmailErrorMessage}" />
    </HorizontalStackLayout>
</VerticalStackLayout>

Специфики на платформите: TalkBack, VoiceOver и Narrator

Въпреки че .NET MAUI абстрахира голяма част от платформените различия, познаването на спецификите на всеки екранен четец е от съществено значение. Всеки от тях има своите особености и капризи.

TalkBack (Android)

TalkBack е екранният четец на Android. Основните жестове включват:

  • Плъзгане наляво/надясно — навигация между елементите
  • Двойно докосване — активиране на избрания елемент
  • Плъзгане с два пръста — превъртане
  • L-образен жест — отваряне на контекстното меню на TalkBack

За Android специфични настройки в .NET MAUI:

// Платформено-специфична конфигурация за Android
#if ANDROID
using Android.Views.Accessibility;

public partial class CustomAccessibleView : ContentView
{
    partial void PlatformSetup()
    {
        var handler = Handler?.PlatformView;
        if (handler != null)
        {
            handler.ImportantForAccessibility =
                Android.Views.ImportantForAccessibility.Yes;

            handler.AccessibilityLiveRegion =
                Android.Views.AccessibilityLiveRegion.Polite;
        }
    }
}
#endif

VoiceOver (iOS)

VoiceOver е екранният четец на Apple за iOS и iPadOS. Ключови жестове:

  • Плъзгане наляво/надясно — навигация между елементите
  • Двойно докосване — активиране на елемента
  • Завъртане с два пръста (ротор) — избор на режим на навигация (заглавия, връзки и др.)
  • Плъзгане нагоре/надолу с три пръста — превъртане

VoiceOver има специална функционалност — ротор, която позволява навигация по заглавия. Точно затова SemanticProperties.HeadingLevel е толкова важен за iOS.

// Платформено-специфична конфигурация за iOS
#if IOS
using UIKit;

public partial class CustomAccessibleView : ContentView
{
    partial void PlatformSetup()
    {
        var handler = Handler?.PlatformView;
        if (handler != null)
        {
            handler.IsAccessibilityElement = true;
            handler.AccessibilityTraits = UIAccessibilityTrait.Button;

            // Дефиниране на персонализирани действия за VoiceOver
            handler.AccessibilityCustomActions = new[]
            {
                new UIAccessibilityCustomAction(
                    "Изтрий",
                    (action) => { DeleteItem(); return true; }),
                new UIAccessibilityCustomAction(
                    "Редактирай",
                    (action) => { EditItem(); return true; })
            };
        }
    }
}
#endif

Narrator (Windows)

Narrator е екранният четец на Windows. Той поддържа навигация с клавиатура:

  • Caps Lock + стрелки — навигация между елементите
  • Enter/Space — активиране на елемента
  • Caps Lock + H — навигация по заглавия
  • Caps Lock + T — четене на заглавието на прозореца

За Windows .NET MAUI приложения, Narrator работи с UIA (UI Automation) дървото, което се генерира автоматично от рамката на базата на зададените семантични свойства. Тук нямате нужда от допълнителни настройки в повечето случаи.

Клавиатурна и алтернативна навигация

Много потребители не могат да използват сензорен екран и разчитат на клавиатура, превключватели (switches) или други алтернативни входни устройства. Осигуряването на пълна клавиатурна навигация е задължително по WCAG 2.2 (критерий 2.1.1 Keyboard, Ниво A).

Управление на реда на фокуса (Tab Order)

<!-- Дефиниране на логичен ред на табулация -->
<VerticalStackLayout>
    <Label Text="Форма за регистрация"
           SemanticProperties.HeadingLevel="Level1"
           FontSize="24" />

    <Entry Placeholder="Потребителско име"
           TabIndex="1"
           SemanticProperties.Description="Потребителско име"
           SemanticProperties.Hint="Въведете желаното потребителско име" />

    <Entry Placeholder="Имейл адрес"
           TabIndex="2"
           Keyboard="Email"
           SemanticProperties.Description="Имейл адрес"
           SemanticProperties.Hint="Въведете вашия имейл адрес" />

    <Entry Placeholder="Парола"
           TabIndex="3"
           IsPassword="True"
           SemanticProperties.Description="Парола"
           SemanticProperties.Hint="Въведете парола с минимум 8 символа" />

    <Entry Placeholder="Потвърдете паролата"
           TabIndex="4"
           IsPassword="True"
           SemanticProperties.Description="Потвърждение на паролата"
           SemanticProperties.Hint="Въведете паролата отново за потвърждение" />

    <Button Text="Регистрация"
            TabIndex="5"
            Command="{Binding RegisterCommand}"
            SemanticProperties.Hint="Натиснете за да създадете нов акаунт" />

    <Button Text="Вече имам акаунт"
            TabIndex="6"
            Command="{Binding NavigateToLoginCommand}"
            SemanticProperties.Hint="Отива на страницата за вход в съществуващ акаунт" />
</VerticalStackLayout>

Програмно управление на фокуса

// Преместване на фокуса програмно
public partial class LoginPage : ContentPage
{
    private async void OnUsernameCompleted(object sender, EventArgs e)
    {
        // Преместване на фокуса към полето за парола
        PasswordEntry.Focus();
    }

    private async void OnLoginFailed(string errorMessage)
    {
        // Показване на грешка и преместване на фокуса
        ErrorLabel.Text = errorMessage;
        ErrorLabel.IsVisible = true;

        // Обявяване на грешката за екранни четци
        SemanticScreenReader.Default.Announce(errorMessage);

        // Преместване на фокуса обратно към потребителското име
        UsernameEntry.Focus();
    }

    // Управление на фокуса при навигация
    protected override void OnAppearing()
    {
        base.OnAppearing();

        // Задаване на начален фокус
        MainThread.BeginInvokeOnMainThread(() =>
        {
            UsernameEntry.Focus();
            SemanticScreenReader.Default.Announce(
                "Страница за вход. Моля, въведете вашите данни.");
        });
    }
}

Достъпни персонализирани жестове

Когато използвате персонализирани жестове, винаги осигурявайте алтернативен начин за същото действие. Това е едно от нещата, които лесно се забравят:

<!-- Swipe за изтриване с алтернативен бутон -->
<SwipeView>
    <SwipeView.RightItems>
        <SwipeItems>
            <SwipeItem Text="Изтрий"
                       BackgroundColor="Red"
                       Command="{Binding DeleteCommand}"
                       SemanticProperties.Description="Изтриване на елемента" />
        </SwipeItems>
    </SwipeView.RightItems>

    <Grid Padding="16" ColumnDefinitions="*, Auto">
        <Label Text="{Binding ItemName}"
               VerticalOptions="Center" />

        <!-- Видим бутон за изтриване като алтернатива на swipe -->
        <ImageButton Source="delete_icon.png"
                     Grid.Column="1"
                     Command="{Binding DeleteCommand}"
                     WidthRequest="48"
                     HeightRequest="48"
                     SemanticProperties.Description="Изтриване"
                     SemanticProperties.Hint="Изтрива текущия елемент от списъка" />
    </Grid>
</SwipeView>

Инструменти за тестване на достъпността

Тестването е многопластов процес, който изисква комбинация от автоматизирани инструменти и ръчно тестване. Нито едното, нито другото самостоятелно не е достатъчно.

Accessibility Inspector (iOS/macOS)

Accessibility Inspector е инструмент на Apple, вграден в Xcode. Той позволява:

  • Инспектиране на свойствата за достъпност на всеки елемент
  • Автоматично сканиране за често срещани проблеми
  • Симулиране на VoiceOver навигация
  • Проверка на контраста на цветовете

За да го използвате с .NET MAUI приложение:

  1. Стартирайте приложението на iOS симулатор
  2. Отворете Xcode → Open Developer Tool → Accessibility Inspector
  3. Изберете симулатора от падащото меню
  4. Използвайте бутона за инспектиране, за да прегледате елементите
  5. Стартирайте Audit за автоматична проверка

Accessibility Scanner (Android)

Google Accessibility Scanner е приложение за Android, което анализира потребителския интерфейс и предоставя конкретни предложения:

  • Проверка на размера на целевите области за докосване
  • Анализ на цветовия контраст
  • Откриване на елементи без описание за достъпност
  • Предложения за подобрения с конкретни препоръки

За да тествате с Accessibility Scanner:

  1. Инсталирайте Accessibility Scanner от Google Play Store
  2. Активирайте го от Настройки → Достъпност
  3. Отворете вашето .NET MAUI приложение
  4. Натиснете плаващия бутон на Scanner за анализ
  5. Прегледайте намерените проблеми и предложените корекции

Ръчно тестване с екранни четци

Автоматичните инструменти не могат да уловят всички проблеми. Ръчното тестване с реални екранни четци е незаменимо. Ето контролен списък, който използвам в моите проекти:

// Контролен списък за ръчно тестване (като коментари в кода)

// 1. Навигация с екранен четец
// [ ] Всички интерактивни елементи са достъпни чрез плъзгане
// [ ] Редът на навигация е логичен
// [ ] Заглавията са правилно маркирани
// [ ] Навигацията по заглавия работи (VoiceOver ротор)

// 2. Описания и подсказки
// [ ] Всички изображения имат описание (или са маркирани като декоративни)
// [ ] Бутоните с икони имат текстово описание
// [ ] Подсказките (Hint) са информативни и точни
// [ ] Динамичните промени се обявяват

// 3. Формуляри
// [ ] Полетата имат достъпни етикети
// [ ] Грешките при валидация се обявяват
// [ ] Фокусът се управлява правилно
// [ ] Клавиатурата не закрива активното поле

// 4. Визуална достъпност
// [ ] Контрастът отговаря на WCAG AA (4.5:1 за текст)
// [ ] Текстът се мащабира правилно при увеличен шрифт
// [ ] Оформлението не се чупи при увеличен шрифт
// [ ] Информацията не се предава само чрез цвят

// 5. Интерактивни елементи
// [ ] Целевите области са достатъчно големи (мин. 24x24, препоръчано 48x48)
// [ ] Swipe действията имат алтернативи
// [ ] Таймаутите са достатъчно дълги или могат да се удължат

Интеграция на тестове за достъпност в CI/CD

Автоматизираните тестове няма да хванат всичко, но пък ще ви предпазят от регресии. Ето как можете да интегрирате проверки за достъпност в CI/CD:

// Unit тест за проверка на семантични свойства
[TestClass]
public class AccessibilityTests
{
    [TestMethod]
    public void ProductCard_ShouldHaveSemanticDescription()
    {
        // Arrange
        var viewModel = new ProductViewModel
        {
            Name = "Безжични слушалки",
            Price = 89.99m
        };

        // Act
        var card = new ProductCard { BindingContext = viewModel };

        // Assert
        var description = SemanticProperties.GetDescription(card);
        Assert.IsFalse(string.IsNullOrEmpty(description),
            "ProductCard трябва да има семантично описание.");
    }

    [TestMethod]
    public void AllButtons_ShouldMeetMinimumTouchTargetSize()
    {
        // Arrange
        var page = new MainPage();
        var buttons = FindAllDescendants<Button>(page);

        // Assert
        foreach (var button in buttons)
        {
            Assert.IsTrue(
                button.MinimumHeightRequest >= 24 || button.HeightRequest >= 24,
                $"Бутон '{button.Text}' не отговаря на минималния размер за докосване.");
            Assert.IsTrue(
                button.MinimumWidthRequest >= 24 || button.WidthRequest >= 24,
                $"Бутон '{button.Text}' не отговаря на минималния размер за докосване.");
        }
    }

    [TestMethod]
    public void ColorPairs_ShouldMeetWcagAAContrast()
    {
        // Проверка на основните цветови комбинации
        var pairs = new[]
        {
            (foreground: Color.FromArgb("#1A1A1A"),
             background: Color.FromArgb("#FFFFFF"),
             name: "Primary text on white"),
            (foreground: Color.FromArgb("#4A4A4A"),
             background: Color.FromArgb("#FFFFFF"),
             name: "Secondary text on white"),
            (foreground: Color.FromArgb("#ECECEC"),
             background: Color.FromArgb("#121212"),
             name: "Primary text on dark")
        };

        foreach (var pair in pairs)
        {
            double ratio = ContrastChecker.GetContrastRatio(
                pair.foreground, pair.background);
            Assert.IsTrue(ratio >= 4.5,
                $"Цветовата комбинация '{pair.name}' има контраст {ratio:F2}:1, " +
                $"което е под изискваното 4.5:1 за WCAG AA.");
        }
    }

    private IEnumerable<T> FindAllDescendants<T>(Element parent) where T : Element
    {
        var results = new List<T>();
        foreach (var child in (parent as IVisualTreeElement)?.GetVisualChildren()
                              ?? Enumerable.Empty<IVisualTreeElement>())
        {
            if (child is T typedChild)
                results.Add(typedChild);
            if (child is Element element)
                results.AddRange(FindAllDescendants<T>(element));
        }
        return results;
    }
}

Често срещани грешки и как да ги избегнете

Нека разгледаме най-честите грешки при разработката на достъпни .NET MAUI приложения. Виждал съм всяка от тях в реални проекти (и да, допускал съм някои от тях и аз).

Грешка 1: Липса на описания за изображения и бутони с икони

<!-- ГРЕШКА: Бутон с икона без описание -->
<ImageButton Source="settings.png" />

<!-- КОРЕКЦИЯ: Добавяне на семантично описание -->
<ImageButton Source="settings.png"
             SemanticProperties.Description="Настройки"
             SemanticProperties.Hint="Отваря страницата с настройки на приложението" />

Грешка 2: Дублиране на информация за екранни четци

<!-- ГРЕШКА: Екранният четец ще прочете "Запази Запази бутон" -->
<Button Text="Запази"
        SemanticProperties.Description="Запази" />

<!-- КОРЕКЦИЯ: Не задавайте Description, когато Text вече е достатъчен -->
<Button Text="Запази"
        SemanticProperties.Hint="Записва промените в профила ви" />

Грешка 3: Неинформативни описания

Това е класика. „Изображение" не казва абсолютно нищо на потребителя.

<!-- ГРЕШКА: Описанието не е полезно -->
<Image Source="chart.png"
       SemanticProperties.Description="Изображение" />

<!-- КОРЕКЦИЯ: Описание, което предава съдържанието -->
<Image Source="chart.png"
       SemanticProperties.Description="Графика на продажбите за 2024 г.,
       показваща ръст от 25% спрямо предходната година" />

Грешка 4: Неправилно скриване на декоративни елементи

<!-- ГРЕШКА: Декоративно изображение е видимо за екранни четци -->
<Image Source="divider_line.png" />

<!-- КОРЕКЦИЯ: Маркиране като невидимо за помощни технологии -->
<Image Source="divider_line.png"
       AutomationProperties.IsInAccessibleTree="False" />

Грешка 5: Липса на обявления при динамични промени

// ГРЕШКА: Потребителят на екранен четец не знае, че нещо се е променило
private async Task RefreshData()
{
    Items = await _service.GetItemsAsync();
}

// КОРЕКЦИЯ: Обявяване на промяната
private async Task RefreshData()
{
    SemanticScreenReader.Default.Announce("Обновяване на данните...");

    Items = await _service.GetItemsAsync();

    SemanticScreenReader.Default.Announce(
        $"Данните бяха обновени. Показани са {Items.Count} елемента.");
}

Грешка 6: Недостъпни персонализирани контроли

Персонализираните контроли са може би най-проблемната област. Ето типичен пример с рейтинг контрол:

<!-- ГРЕШКА: Персонализиран контрол без семантика за достъпност -->
<Grid>
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Tapped="OnRatingTapped" />
    </Grid.GestureRecognizers>
    <HorizontalStackLayout>
        <Image Source="star_filled.png" />
        <Image Source="star_filled.png" />
        <Image Source="star_filled.png" />
        <Image Source="star_empty.png" />
        <Image Source="star_empty.png" />
    </HorizontalStackLayout>
</Grid>

<!-- КОРЕКЦИЯ: Добавяне на пълна семантика -->
<Grid SemanticProperties.Description="Оценка: 3 от 5 звезди"
      SemanticProperties.Hint="Докоснете за да промените оценката">
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Tapped="OnRatingTapped" />
    </Grid.GestureRecognizers>
    <HorizontalStackLayout AutomationProperties.ExcludedWithChildren="True">
        <Image Source="star_filled.png" />
        <Image Source="star_filled.png" />
        <Image Source="star_filled.png" />
        <Image Source="star_empty.png" />
        <Image Source="star_empty.png" />
    </HorizontalStackLayout>
</Grid>

Грешка 7: Игнориране на мащабирането при критични елементи

<!-- ГРЕШКА: Фиксирана височина, която не позволява на текста да се разшири -->
<Frame HeightRequest="50">
    <Label Text="Дълъг текст, който може да нарасне при увеличен шрифт"
           FontSize="16" />
</Frame>

<!-- КОРЕКЦИЯ: Минимална височина вместо фиксирана -->
<Frame MinimumHeightRequest="50">
    <Label Text="Дълъг текст, който може да нарасне при увеличен шрифт"
           FontSize="16"
           LineBreakMode="WordWrap" />
</Frame>

Обобщение на най-добрите практики

И така, ето какво трябва да запомните:

  • Задавайте семантични свойства на всички значими елементи — всяко изображение, бутон с икона и интерактивен елемент трябва да има описание
  • Скривайте декоративните елементи — използвайте AutomationProperties.IsInAccessibleTree="False" за елементи без информационна стойност
  • Групирайте свързани елементи — вместо да четете пет отделни текста, групирайте ги с едно обобщено описание
  • Обявявайте динамичните промени — използвайте SemanticScreenReader.Default.Announce() за информиране при промени
  • Тествайте с реални екранни четци — автоматичните инструменти не улавят всичко
  • Осигурете достатъчен контраст — 4.5:1 за текст, 3:1 за UI компоненти
  • Използвайте достатъчно големи целеви области — минимум 24x24, препоръчано 48x48 пиксела
  • Поддържайте мащабиране на шрифтове — не деактивирайте FontAutoScalingEnabled без основателна причина
  • Осигурете клавиатурна навигация — задайте логичен TabIndex и управлявайте фокуса програмно
  • Не разчитайте само на цвят — добавяйте текст или икони като допълнителен индикатор
  • Използвайте заглавияSemanticProperties.HeadingLevel позволява бърза навигация
  • Тествайте при максимален шрифт — увеличете шрифта до максимум и проверете оформлението

Цялостен пример: Достъпна страница с продукт

За да обединим всичко от ръководството, нека разгледаме пример за напълно достъпна страница с детайли за продукт. Това е нещо, което можете да вземете и адаптирате за вашия проект:

<?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="MyShop.Views.ProductDetailPage"
             Title="Детайли за продукта">

    <ScrollView>
        <VerticalStackLayout Padding="16" Spacing="16">

            <!-- Заглавие на продукта -->
            <Label Text="{Binding Product.Name}"
                   FontSize="28"
                   FontAttributes="Bold"
                   SemanticProperties.HeadingLevel="Level1" />

            <!-- Изображение на продукта -->
            <Image Source="{Binding Product.ImageUrl}"
                   HeightRequest="250"
                   Aspect="AspectFit"
                   SemanticProperties.Description="{Binding Product.ImageDescription}" />

            <!-- Цена и наличност -->
            <HorizontalStackLayout Spacing="12">
                <Label Text="{Binding Product.Price, StringFormat='{0:C}'}"
                       FontSize="24"
                       FontAttributes="Bold"
                       TextColor="{StaticResource PrimaryLight}"
                       SemanticProperties.Description="{Binding PriceAccessibilityText}" />

                <Frame BackgroundColor="{Binding AvailabilityColor}"
                       Padding="8,4"
                       CornerRadius="4"
                       SemanticProperties.Description="{Binding AvailabilityAccessibilityText}">
                    <Label Text="{Binding AvailabilityText}"
                           TextColor="White"
                           FontSize="12"
                           AutomationProperties.IsInAccessibleTree="False" />
                </Frame>
            </HorizontalStackLayout>

            <!-- Описание -->
            <Label Text="Описание"
                   FontSize="20"
                   FontAttributes="Bold"
                   SemanticProperties.HeadingLevel="Level2" />

            <Label Text="{Binding Product.Description}"
                   FontSize="16"
                   LineBreakMode="WordWrap" />

            <!-- Избор на количество -->
            <Label Text="Количество"
                   FontSize="20"
                   FontAttributes="Bold"
                   SemanticProperties.HeadingLevel="Level2" />

            <HorizontalStackLayout Spacing="12"
                                   HorizontalOptions="Start">
                <Button Text="-"
                        Command="{Binding DecreaseQuantityCommand}"
                        WidthRequest="48"
                        HeightRequest="48"
                        SemanticProperties.Description="Намаляване на количеството"
                        SemanticProperties.Hint="Намалява количеството с единица" />

                <Label Text="{Binding Quantity}"
                       FontSize="20"
                       VerticalOptions="Center"
                       MinimumWidthRequest="48"
                       HorizontalTextAlignment="Center"
                       SemanticProperties.Description="{Binding Quantity,
                           StringFormat='Текущо количество: {0}'}" />

                <Button Text="+"
                        Command="{Binding IncreaseQuantityCommand}"
                        WidthRequest="48"
                        HeightRequest="48"
                        SemanticProperties.Description="Увеличаване на количеството"
                        SemanticProperties.Hint="Увеличава количеството с единица" />
            </HorizontalStackLayout>

            <!-- Бутон за добавяне в количката -->
            <Button Text="Добави в количката"
                    Command="{Binding AddToCartCommand}"
                    FontSize="18"
                    Padding="16,14"
                    MinimumHeightRequest="48"
                    BackgroundColor="{StaticResource PrimaryLight}"
                    TextColor="White"
                    SemanticProperties.Hint="{Binding AddToCartHint}" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

И съответният ViewModel с достъпни обявления:

public partial class ProductDetailViewModel : ObservableObject
{
    [ObservableProperty]
    private Product _product;

    [ObservableProperty]
    private int _quantity = 1;

    public string PriceAccessibilityText =>
        $"Цена: {Product?.Price:C}";

    public string AvailabilityAccessibilityText =>
        Product?.IsInStock == true
            ? "Наличен в склада"
            : "Изчерпан, очаква се доставка";

    public string AvailabilityText =>
        Product?.IsInStock == true ? "В наличност" : "Изчерпан";

    public Color AvailabilityColor =>
        Product?.IsInStock == true
            ? Color.FromArgb("#2E7D32")
            : Color.FromArgb("#C62828");

    public string AddToCartHint =>
        $"Добавя {Quantity} броя {Product?.Name} в кошницата за пазаруване";

    [RelayCommand]
    private void IncreaseQuantity()
    {
        if (Quantity < Product.MaxQuantity)
        {
            Quantity++;
            SemanticScreenReader.Default.Announce(
                $"Количество: {Quantity}");
        }
        else
        {
            SemanticScreenReader.Default.Announce(
                $"Достигнато е максималното количество от {Product.MaxQuantity} броя.");
        }
    }

    [RelayCommand]
    private void DecreaseQuantity()
    {
        if (Quantity > 1)
        {
            Quantity--;
            SemanticScreenReader.Default.Announce(
                $"Количество: {Quantity}");
        }
        else
        {
            SemanticScreenReader.Default.Announce(
                "Минималното количество е 1.");
        }
    }

    [RelayCommand]
    private async Task AddToCart()
    {
        SemanticScreenReader.Default.Announce("Добавяне в количката...");

        bool success = await _cartService.AddAsync(Product, Quantity);

        if (success)
        {
            SemanticScreenReader.Default.Announce(
                $"{Quantity} броя {Product.Name} бяха добавени в количката. " +
                $"Обща сума в количката: {_cartService.TotalPrice:C}.");
        }
        else
        {
            SemanticScreenReader.Default.Announce(
                "Грешка при добавяне в количката. Моля, опитайте отново.");
        }
    }
}

Заключение

Достъпността в .NET MAUI приложенията не е опция — тя е необходимост. С нарастващите законови изисквания и етичните съображения, всеки разработчик трябва да интегрира практиките за достъпност в ежедневния си работен процес.

.NET MAUI ни дава доста изчерпателен набор от инструменти. SemanticProperties осигуряват семантична информация за екранните четци, AutomationProperties дават фин контрол над дървото за достъпност, а SemanticScreenReader позволява програмни обявления за динамични промени.

Ключовите принципи, които трябва да запомните:

  1. Проектирайте с мисъл за достъпност от самото начало — добавянето й след факта е многократно по-скъпо и по-малко ефективно
  2. Следвайте WCAG 2.2 стандарта — той предоставя ясни, измерими критерии за съответствие
  3. Тествайте с реални помощни технологии — TalkBack на Android, VoiceOver на iOS и Narrator на Windows
  4. Автоматизирайте когато е възможно — включете проверки за достъпност в CI/CD процеса
  5. Включете потребители с увреждания в тестването — никой инструмент не може да замени реалния потребителски опит

Достъпността е пътуване, а не дестинация. Всяко малко подобрение прави приложението ви по-достъпно за повече хора. Започнете с основите — семантични описания, адекватен контраст и достатъчно големи целеви области — и постепенно усъвършенствайте всичко с всяка нова версия.

Като .NET MAUI разработчици имаме уникалната възможност да напишем код веднъж и да осигурим достъпност на множество платформи едновременно. Нека не пропускаме тази възможност.

За Автора Devika Ramaswamy

Devika spent four years on the Xamarin team at Microsoft before the transition to .NET MAUI, where she worked on the iOS handler layer and shipped fixes that landed in the .NET 7 and .NET 8 release notes. She left Redmond in 2023 to run mobile engineering at a Series B logistics startup, porting their 600k-line Xamarin.Forms codebase to MAUI over eleven months. She writes mostly about the unglamorous parts of cross-platform work: handler internals, AOT trimming on iOS, MSBuild target customization, and why your hot reload keeps breaking. She holds the .NET MAUI MVP award (2024, 2025) and has spoken at .NET Conf and Xamarin Expert Day. Based in Bengaluru, she still pushes the occasional PR to the dotnet/maui repo on weekends.