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

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

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

Достъпността (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 разработчици имаме уникалната възможност да напишем код веднъж и да осигурим достъпност на множество платформи едновременно. Нека не пропускаме тази възможност.

За Автора Editorial Team

Our team of expert writers and editors.