Angular IDPStrong Junior
🔥 0

Модуль 7 · урок 6 з 10

Клавіатура і фокус

«Main user flows can be completed purely via keyboard» — критерій приймання дослівно. Це найлегша частина доступності для перевірки: сідаєш, ховаєш мишу й проходиш екран. І найчастіше на цьому все ламається.

Порядок табуляції

<button>         <!-- у порядку табуляції за замовчуванням -->
<a href>
<input> <select> <textarea>
<div tabindex="0">   <!-- додати вручну -->

<div tabindex="-1">  <!-- фокус лише програмно, Tab пропускає -->
<button disabled>    <!-- випадає з порядку -->
Ніколи не пиши tabindex більше нуля

tabindex="1" та більші виривають елемент із природного порядку й ставлять його перед усіма іншими. Один такий атрибут ламає навігацію на всій сторінці, і причину потім важко знайти. Порядок табуляції має задаватись порядком елементів у розмітці — це ще один аргумент за чисте дерево DOM із модуля 1.

Видимий фокус

/* ❌ класичний злочин */
*:focus { outline: none; }

/* ✅ стилізувати, а не прибирати */
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

Це вже було в модулі 1, але повторити варто: без видимого фокуса клавіатурна навігація перетворюється на гру наосліп. :focus-visible показує обведення лише тоді, коли фокус прийшов із клавіатури, тож дизайнерам немає на що скаржитись.

Skip link

<a class="skip-link" href="#main">Перейти до вмісту</a>
<app-header />
<app-sidebar />
<main id="main" tabindex="-1">…</main>
.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: 8px; top: 8px;
  z-index: 100;
  padding: 8px 12px;
  background: var(--color-surface);
}

Без нього клавіатурний користувач щоразу проходить двадцять пунктів меню, щоб дістатись таблиці. Перший Tab на сторінці показує посилання — і один Enter переносить одразу до вмісту.

Пастка фокуса в модалці

СТОРІНКА ПІД МОДАЛКОЮ — НЕДОСЯЖНА ДЛЯ TAB МОДАЛКА поле 1 поле 2 Зберегти Tab з останнього → на перший Escape закриває · при відкритті фокус у перше поле · при закритті повертається на кнопку, що відкрила
Фокус ходить по колу всередині модалки й не виходить за її межі. CDK робить це сам — тому власні модалки й не варті зусиль.
// усе це вже вбудовано в MatDialog з модуля 6
this.dialog.open(TaskFormDialog, {
  autoFocus: 'first-tabbable',
  restoreFocus: true,
});

Клавіатура в таблиці

<tr [attr.tabindex]="0"
    (click)="open(row)"
    (keydown.enter)="open(row)"
    (keydown.space)="open(row); $event.preventDefault()">

Якщо рядок клікабельний, він має бути й фокусованим. preventDefault для пробілу обовʼязковий: інакше сторінка прокрутиться вниз замість відкриття рядка.

// у панелі інструментів — стрілки замість Tab (патерн roving tabindex)
<div role="toolbar" (keydown)="onToolbarKey($event)">
  @for (action of actions; track action.id; let i = $index) {
    <button [attr.tabindex]="i === activeIndex() ? 0 : -1">{{ action.label }}</button>
  }
</div>

Це не обовʼязково для IDP, але корисно знати: у групах однотипних кнопок прийнято, щоб Tab заходив у групу один раз, а всередині працювали стрілки. Так навігація по панелі з десяти кнопок не забирає десять натискань Tab.

Гарячі клавіші

@HostListener('document:keydown', ['$event'])
onKey(e: KeyboardEvent): void {
  // не перехоплювати, коли людина щось друкує
  const tag = (e.target as HTMLElement).tagName;
  if (tag === 'INPUT' || tag === 'TEXTAREA') return;

  if (e.key === '/') {
    e.preventDefault();
    this.searchInput()?.nativeElement.focus();
  }
}

Як перевірити за пʼять хвилин

  1. Прибери руку з миші. Зовсім.
  2. Tab від початку сторінки: фокус має бути видно завжди.
  3. Порядок має відповідати візуальному: зліва направо, згори вниз.
  4. Пройди основний сценарій: знайти запис, відкрити, змінити, зберегти.
  5. Відкрий модалку: фокус усередині, Escape закриває, Tab не вилітає назовні.
  6. Після закриття фокус повернувся туди, звідки відкривали.

Якщо всі шість пунктів проходять — критерій із таблиці виконано. Це буквально пʼять хвилин роботи, а на демо виглядає переконливо: мало хто взагалі про це думає.