Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

Специальные возможности

NSH отрисовывает интерфейс нативными представлениями, поэтому вспомогательные технологии платформ (VoiceOver на iOS и Mac Catalyst, TalkBack на Android, Switch Control, Voice Control, …) видят настоящую иерархию представлений вашего приложения. Вы добавляете то, что эти технологии не могут определить сами: чем является элемент, что в нём написано, в каком он состоянии и что он умеет. Каждый элемент принимает перечисленные ниже методы (Rust) и свойства (TSX).

#Что вы получаете без дополнительных усилий

  • Текст озвучивается. Текстовый элемент является элементом доступности, а его метка совпадает с текстом. Представление без метки озвучивается как тексты внутри него.
  • Нажимаемые представления являются элементами. Представление с обработчиком нажатия или долгого нажатия (.on_press, onPress) по умолчанию является элементом доступности, а его активация (двойное касание в VoiceOver или TalkBack) нажимает его.
  • Поля ввода являются текстовыми полями с подсказкой, значением и клавиатурой.
  • Текст следует настройке размера, выбранной пользователем. См. Размер текста.

#Метки, подсказки и роли

view()
    .on_press(play)
    .role(AccessibilityRole::Button)
    .label("Play")
    .hint("Starts the song")
    .size(px(44.0))
    .items_center()
    .justify_center()
    .child(text("▶"))
RustTSXТипОписание
.accessible(..)accessiblebooleanПредставление является одним элементом для вспомогательных технологий; его дочерние элементы не передаются отдельно.
.label(..)accessibilityLabelstringЧто читают скринридеры вместо текстов внутри.
.hint(..)accessibilityHintstringЧто делает активация элемента; читается после паузы.
.role(..)roleAccessibilityRoleЧем является элемент.
.accessibility_state(..)accessibilityStateAccessibilityStateЕго состояние: disabled, selected, checked, busy, expanded, pressed, current. selected также применяет стиль выбранного элемента, checked — стиль отмеченного при true и стиль неопределённого состояния при "mixed", pressed — стиль нажатого при true.
.accessibility_level(..)accessibilityLevelnumberУровень заголовка (1 — верхний), глубина элемента дерева (1 — верхний уровень).
.accessibility_value(..)accessibilityValue{ min?, max?, now?, text? }Значение диапазонного или текстового элемента.
accessibilityActions{ name, label? }[]Действия, предлагаемые вспомогательным технологиям.
.on_accessibility_action(..)onAccessibilityAction(e: { actionName }) => voidДействие выполнено (в Rust: его имя).
.accessibility_elements_hidden(..)accessibilityElementsHiddenbooleanСкрывает представление и его поддерево от вспомогательных технологий.
importantForAccessibility"auto" | "yes" | "no" | "no-hide-descendants"Доступно ли представление (и его поддерево) вспомогательным технологиям.
android={{ accessibilityLiveRegion }}"none" | "polite" | "assertive"Android: как TalkBack объявляет изменения представления.
.test_id(..)testIDstringИдентификатор для UI-тестов (accessibility identifier на iOS, resource id на Android); не озвучивается.

Задавайте метку для каждого элемента управления, состоящего только из значка: скринридер не может прочитать глиф или изображение. Делайте метки короткими и не включайте в них роль («Play», а не «Кнопка Play»): роль объявляется отдельно.

#Роли

Роль (.role(AccessibilityRole::Header), role="header") сообщает пользователю, что это за элемент и как с ним работать. В Rust это вариант AccessibilityRole (Button, Header, Switch, …); значения в TSX: "none", "button", "link", "search", "image", "imagebutton", "keyboardkey", "text", "adjustable", "header", "summary", "alert", "checkbox", "combobox", "menu", "menubar", "menuitem", "progressbar", "radio", "radiogroup", "scrollbar", "spinbutton", "switch", "tab", "tablist", "timer", "toolbar", "list", "grid", "table", "rowgroup", "row", "columnheader" и "cell" (роли частей Table), "listbox" и "option" (список вариантов для выбора, например список Select, и его варианты, выбранный отмечается accessibilityState.selected), "group" (связанные элементы, составляющие единое целое), "region" (раздел, к которому стоит переходить отдельно, например панель аккордеона), "navigation" (ссылки для перемещения по приложению, например пагинация), "tree" и "treeitem" (элементы, которые разворачиваются и сворачиваются, каждый на своём accessibilityLevel), "menuitemcheckbox" и "menuitemradio" (пункты меню с отметкой, отдельные или как один из вариантов группы).

Наиболее полезные:

  • "button" для любого нажимаемого элемента, не являющегося ссылкой; "link" для перехода к содержимому.
  • "header" для заголовков разделов: пользователи скринридеров переходят между заголовками, чтобы бегло просмотреть экран. accessibilityLevel задаёт его уровень (1 — верхний) там, где платформа его читает.
  • "button" вместе с accessibilityState.pressed для кнопки-переключателя, которая остаётся включённой или выключенной (полужирный шрифт на панели инструментов).
  • "navigation" вокруг ссылок, переключающих страницы, со accessibilityState.current: "page" у ссылки на показанную страницу.
  • "switch", "checkbox", "radio" вместе с accessibilityState.checked.
  • "adjustable" для слайдеров и степперов, со accessibilityValue и действиями increment / decrement (каждый ползунок Slider является таким элементом).
  • "spinbutton" для поля, которое хранит число, которое пользователь также может изменять шагами, с тем же значением и действиями (поле NumberInput).
  • "image" для значимых изображений, с меткой; декоративные изображения лучше скрывать.
text("Settings")
    .role(AccessibilityRole::Header)
    .text_size(px(22.0))
    .font_weight(FontWeight::BOLD)

#Состояния и значения

Для элемента управления «вкл / выкл» в приложении используйте Switch из nsh/base, который сам задаёт роль и состояние. Пример показывает, что делают роль и состояние на представлении.

use nsh_core::{Checked, prelude::*};

// A toggle drawn with views.
view()
    .on_press(move || on.update(|v| *v = !*v))
    .role(AccessibilityRole::Switch)
    .label("Wi-Fi")
    .accessibility_state(move || AccessibilityState {
        checked: Some(if on.get() { Checked::True } else { Checked::False }),
        ..AccessibilityState::default()
    })
    .hit_slop(EdgeInsets::all(px(8.0)))
    .w(px(51.0))
    .h(px(31.0))
    .rounded_full()
    .p(px(2.0))
    .bg(move || if on.get() { rgb(0x22c55e) } else { rgb(0xd4d4d8) })
    .child(
        view()
            .size(px(27.0))
            .rounded_full()
            .bg(white())
            .style_with(move |s| s.translate_x(px(if on.get() { 20.0 } else { 0.0 }))),
    )

Состояние задаётся через AccessibilityState { disabled, selected, checked: Option<Checked>, busy, expanded, pressed: Option<Checked>, current: Option<AccessibilityCurrent> } в Rust и { disabled?, selected?, checked?: boolean | "mixed", busy?, expanded?, pressed?: boolean | "mixed", current?: boolean | "page" | "step" | "location" | "date" | "time" } в TSX. pressed превращает кнопку в кнопку-переключатель (aria-pressed из ARIA); current отмечает текущий элемент набора (aria-current из ARIA): страницу пагинации, шаг мастера. disabled также подразумевается при отключении представления (.disabled(..), disabled). accessibilityValue вместе с min, max и now читается как процент; с text читается как задано («3 из 10», «Medium»).

#Действия

Нажимаемое представление уже поддерживает активацию. Для всего остального объявляйте действия через accessibilityActions и обрабатывайте их через onAccessibilityAction; компонент на Rust получает имя выполненного действия через .on_accessibility_action(|name: &str| ..):

function Quantity() {
  const n = signal(1);
  return (
    <View
      accessible
      role="adjustable"
      accessibilityLabel="Quantity"
      accessibilityValue={{ text: String(n()) }}
      accessibilityActions={[{ name: "increment" }, { name: "decrement" }]}
      onAccessibilityAction={(e) => {
        if (e.actionName === "increment") n.update((v) => v + 1);
        if (e.actionName === "decrement") n.update((v) => Math.max(0, v - 1));
      }}
      style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
    >
      <View onPress={() => n.update((v) => Math.max(0, v - 1))}><Text>−</Text></View>
      <Text>{n()}</Text>
      <View onPress={() => n.update((v) => v + 1)}><Text>+</Text></View>
    </View>
  );
}

Некоторые имена действий стандартные и соответствуют собственным жестам платформ:

ИмяiOS (VoiceOver)Android (TalkBack)
activateДвойное касаниеДвойное касание (click)
increment, decrementСвайп вверх / вниз, затем VoiceOver читает новое значениеПрокрутка вперёд / назад
longpressДвойное касание с удержанием
expand, collapseРазвернуть / свернуть
magicTapДвойное касание двумя пальцами
escapeЖест «Z» двумя пальцами

Элемент с increment или decrement регулируется независимо от роли: диаграмма сохраняет роль "image" и перебирает свои точки свайпами VoiceOver и прокруткой TalkBack, а её accessibilityValue читается после каждого шага.

Любое другое имя становится пользовательским действием, которое отображается в роторе действий VoiceOver и в меню действий TalkBack со своим label (или именем). Пользовательские действия — это доступная форма жеста «смахнуть для удаления» и подобных:

<View
  accessibilityActions={[{ name: "archive", label: "Archive" }, { name: "delete", label: "Delete" }]}
  onAccessibilityAction={(e) => (e.actionName === "archive" ? archive(mail.id) : remove(mail.id))}
>
  …
</View>

#Группировка и скрытие

Если сделать представление доступным (.accessible(true), accessible), оно становится одним элементом: карточка с заголовком, подзаголовком и ценой читается за один раз, и пользователю не нужно перебирать свайпами три текста.

view()
    .accessible(true)
    .on_press(open)
    .role(AccessibilityRole::Button)
    .p(px(16.0))
    .child(text("Blue jacket"))
    .child(text("Size M"))
    .child(text("$120"))

Скрывайте то, что ничего не добавляет: декоративные изображения, разделители, значок рядом с текстом, который говорит то же самое.

<Image src={decoration} accessibilityElementsHidden importantForAccessibility="no-hide-descendants" />

accessibilityElementsHidden (название из iOS) и importantForAccessibility="no-hide-descendants" (название из Android) скрывают поддерево на обеих платформах; задавать оба безвредно.

#Скринридер и настройки пользователя

В Rust prelude сообщает настройки пользователя и взаимодействует со скринридером: screen_reader() и reduce_motion() реактивны, announce_for_accessibility(message) заставляет скринридер прочитать сообщение, а element_ref() элемента перемещает фокус скринридера на него с помощью set_accessibility_focus(). В TSX API доступности nsh сообщает настройки пользователя и позволяет обращаться к скринридеру.

ЧленОписание
screenReaderEnabled()Запущен ли VoiceOver или TalkBack (реактивно).
reduceMotion()Просил ли пользователь уменьшить анимацию (реактивно).
announce(message)Скринридер читает message.
setAccessibilityFocus(ref)Перемещает фокус скринридера на представление.

Настройки представлены аксессорами: читайте их в привязке или эффекте, и всё, что их читает, обновится при изменении настройки пользователем, без очистки.

use nsh_core::prelude::*;

fn upload() -> impl IntoElement {
    let done = signal(false);
    let result = element_ref();
    let finish = move || {
        done.set(true);
        announce_for_accessibility("Upload complete");
        result.set_accessibility_focus();
    };
    view()
        .gap(px(12.0))
        // Autoplaying carousels are hard to use with a screen reader: show a list instead.
        .child(when(move || !screen_reader(), photo_carousel).otherwise(photo_list))
        .child(
            text(move || if done.get() { "Uploaded 3 photos" } else { "Uploading…" })
                .element_ref(&result),
        )
}

Объявляйте изменения, которые пользователь иначе пропустил бы: завершение фонового сохранения, ошибку, появившуюся вдали от фокуса, окончание таймера. На Android android={{ accessibilityLiveRegion: "polite" }} на представлении заставляет TalkBack самостоятельно объявлять его изменения.

#Размер текста

Текст и поля ввода следуют системному размеру текста (Dynamic Type на iOS, размер шрифта на Android), если вы это не отключите (.allow_font_scaling(false), allowFontScaling={false}). Вместо отключения лучше учитывать это при проектировании:

  • Позволяйте тексту переноситься: избегайте фиксированной высоты у представлений с текстом.
  • Ограничивайте очень крупные заголовки с помощью .max_font_size_multiplier(..) / maxFontSizeMultiplier вместо отключения масштабирования.
  • Меняйте раскладку при крупном тексте: читайте font_scale() (Rust) или fontScale() из nsh (TSX) (например, располагайте метку и её значение вертикально при значении выше 1.3).

См. Устройство и окружение.

#Уменьшение анимации

reduce_motion() (Rust) и reduceMotion() (TSX) равны true, когда пользователь включил Reduce Motion на iOS или настройку удаления анимации на Android. Ваши анимации выполняются так, как вы их написали, поэтому используйте это значение или варианты Tailwind motion-reduce: и motion-safe:, чтобы заменить крупное движение плавным появлением или мгновенным изменением:

use nsh_core::{element::LayoutAnimation, prelude::*};

let toggle = move || {
    if !reduce_motion() {
        animate_layout(LayoutAnimation::SPRING);
    }
    expanded.update(|e| *e = !*e);
};

view()
    .on_press(toggle)
    .when(!reduce_motion(), |v| v.active(|s| s.scale(0.95)))

#Области касания

Делайте нажимаемые элементы не меньше 44 × 44 пунктов. Если элемент управления должен выглядеть меньше, расширяйте его область касания с помощью hit slop (.hit_slop(..), hitSlop), а не его размер; см. Касания и жесты.

#Проверка приложения

  • Включите VoiceOver (Настройки iOS, Универсальный доступ, или тройное нажатие боковой кнопки, если оно настроено) или TalkBack и пользуйтесь приложением, не глядя на экран: каждый элемент управления должен сообщать, чем он является и что делает.
  • В симуляторах iOS Accessibility Inspector из Xcode показывает метку, трейты и значение каждого элемента.
  • Попробуйте самый большой размер текста и тёмную тему.
  • Тестовый идентификатор (.test_id(..), testID) даёт UI-тестам стабильную точку привязки, которая не меняется в зависимости от языка метки.

#См. также