Навигация между вкладками

Компонент вкладок (tabs) используется для отображения нескольких логически связанных разделов интерфейса в одном и том же пространстве страницы. Каждая вкладка представляет отдельную панель содержимого, а переключение между ними выполняется без перезагрузки документа.

Такая структура широко применяется в веб-приложениях, административных панелях, настройках программ и сложных интерфейсах. Однако реализация вкладок должна учитывать не только визуальное взаимодействие, но и корректную работу клавиатурной навигации, фокуса и вспомогательных технологий.

В среде доступности вкладки рассматриваются как составной интерактивный виджет, состоящий из:

  • контейнера списка вкладок
  • отдельных элементов вкладок
  • панелей содержимого

В спецификации WAI-ARIA эти элементы обычно обозначаются ролями:

  • tablist
  • tab
  • tabpanel

При разработке доступных вкладок необходимо обеспечить:

  • корректное управление фокусом
  • навигацию с клавиатуры
  • синхронизацию активного состояния
  • скрытие неактивных панелей для скринридеров

Библиотека ally.js предоставляет набор инструментов, облегчающих реализацию подобных механизмов.


Основные проблемы навигации между вкладками

При создании вкладочного интерфейса часто возникают следующие сложности:

1. Управление фокусом

При переключении вкладки необходимо:

  • переместить фокус на выбранную вкладку
  • при необходимости переместить фокус внутрь панели

2. Навигация стрелками

Согласно рекомендациям ARIA:

  • и переключают вкладки
  • Home — переход к первой вкладке
  • End — переход к последней вкладке

3. Изоляция неактивных панелей

Неактивное содержимое должно:

  • быть скрытым визуально
  • не попадать в порядок табуляции
  • не восприниматься скринридерами

4. Поддержка различных источников фокуса

Переходы могут происходить через:

  • клавиатуру
  • мышь
  • программное управление

Модульная архитектура ally.js помогает контролировать эти аспекты.


Базовая структура вкладок

Минимальная доступная структура вкладочного интерфейса:

<div class="tabs" role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    Общие
  </button>

  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
    Настройки
  </button>

  <button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3">
    Безопасность
  </button>
</div>

<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
  Содержимое первой вкладки
</div>

<div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
  Содержимое второй вкладки
</div>

<div id="panel-3" role="tabpanel" aria-labelledby="tab-3" hidden>
  Содержимое третьей вкладки
</div>

Ключевые атрибуты:

Атрибут Назначение
role="tablist" контейнер вкладок
role="tab" элемент вкладки
role="tabpanel" панель содержимого
aria-controls связь вкладки и панели
aria-selected активная вкладка

Управление фокусом с ally.js

Вкладки требуют точного контроля над тем, какой элемент получает фокус. В этом помогает модуль ally.element.focus.

Подключение:

import ally from 'ally.js';

Перемещение фокуса:

ally.element.focus(tabElement);

Такой подход гарантирует корректное поведение во всех браузерах, включая ситуации с:

  • отключённым tabindex
  • скрытыми элементами
  • программным управлением

Поиск всех вкладок

Для управления переключением необходимо получить список вкладок.

const tabs = document.querySelectorAll('[role="tab"]');

Однако важно учитывать, какие элементы могут получать фокус. В этом помогает модуль:

ally.query.tabsequence

Он возвращает элементы в реальном порядке табуляции.

Пример:

const focusable = ally.query.tabsequence({
  context: document.querySelector('.tabs')
});

Это позволяет работать только с элементами, которые реально участвуют в навигации.


Переключение активной вкладки

Основная логика переключения включает:

  1. деактивацию текущей вкладки
  2. активацию новой
  3. обновление ARIA-атрибутов
  4. отображение соответствующей панели

Пример функции:

function activateTab(tab) {
  const current = document.querySelector('[role="tab"][aria-selected="true"]');

  if (current) {
    current.setAttribute('aria-selected', 'false');

    const panel = document.getElementById(
      current.getAttribute('aria-controls')
    );

    panel.hidden = true;
  }

  tab.setAttribute('aria-selected', 'true');

  const panel = document.getElementById(
    tab.getAttribute('aria-controls')
  );

  panel.hidden = false;

  ally.element.focus(tab);
}

Навигация стрелками

ARIA-рекомендации определяют клавиатурное управление вкладками.

Основные клавиши:

Клавиша Действие
ArrowRight следующая вкладка
ArrowLeft предыдущая вкладка
Home первая вкладка
End последняя вкладка

Обработчик:

document.querySelector('.tabs').addEventListener('keydown', event => {
  const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
  const index = tabs.indexOf(document.activeElement);

  if (event.key === 'ArrowRight') {
    const next = tabs[(index + 1) % tabs.length];
    activateTab(next);
  }

  if (event.key === 'ArrowLeft') {
    const prev = tabs[(index - 1 + tabs.length) % tabs.length];
    activateTab(prev);
  }

  if (event.key === 'Home') {
    activateTab(tabs[0]);
  }

  if (event.key === 'End') {
    activateTab(tabs[tabs.length - 1]);
  }
});

Использование ally.element.focus() внутри activateTab() гарантирует корректное поведение фокуса.


Исключение неактивных панелей из табуляции

Когда панель скрыта, её содержимое не должно попадать в навигацию клавишей Tab.

Для решения используется модуль:

ally.maintain.disabled

Он временно деактивирует все интерактивные элементы внутри контейнера.

Пример:

const disabled = ally.maintain.disabled({
  context: document.getElementById('panel-2')
});

Позже ограничения можно снять:

disabled.disengage();

Этот механизм удобен при переключении вкладок.


Контроль фокуса внутри активной панели

Иногда требуется автоматически переместить фокус внутрь панели, а не оставлять его на вкладке.

Для этого можно найти первый фокусируемый элемент.

const first = ally.query.firstTabbable({
  context: panel
});

Перемещение фокуса:

if (first) {
  ally.element.focus(first);
}

Это особенно важно для:

  • форм настроек
  • диалоговых интерфейсов
  • панелей с большим количеством элементов

Отслеживание текущего фокуса

При сложных интерфейсах полезно отслеживать изменения фокуса. В ally.js для этого используется:

ally.observe.shadowMutations

или стандартные события focusin.

Пример:

document.addEventListener('focusin', event => {
  console.log('Фокус:', event.target);
});

Это помогает выявлять ошибки навигации.


Поддержка динамического добавления вкладок

Вкладки могут создаваться динамически — например, при открытии новых документов.

После добавления элемента необходимо:

  1. назначить роли
  2. связать вкладку и панель
  3. обновить список вкладок

Пример:

function addTab(title, content) {
  const id = Date.now();

  const tab = document.createElement('button');
  tab.setAttribute('role', 'tab');
  tab.id = `tab-${id}`;
  tab.setAttribute('aria-controls', `panel-${id}`);
  tab.textContent = title;

  const panel = document.createElement('div');
  panel.id = `panel-${id}`;
  panel.setAttribute('role', 'tabpanel');
  panel.setAttribute('aria-labelledby', tab.id);
  panel.hidden = true;
  panel.textContent = content;

  document.querySelector('.tabs').appendChild(tab);
  document.body.appendChild(panel);
}

После добавления вкладка становится частью общей системы навигации.


Вертикальные вкладки

Иногда вкладки располагаются вертикально. В этом случае меняется управление клавиатурой:

Клавиша Действие
ArrowDown следующая вкладка
ArrowUp предыдущая вкладка

ARIA указывает использовать атрибут:

aria-orientation="vertical"

Контейнер вкладок:

<div role="tablist" aria-orientation="vertical">

JavaScript-логика при этом остаётся почти идентичной.


Типичные ошибки реализации

1. Использование <div> без ролей

Без ролей интерфейс становится недоступным для скринридеров.

2. Отсутствие управления фокусом

Фокус может оставаться на скрытом элементе.

3. Неправильная работа клавиатуры

Некоторые реализации поддерживают только мышь.

4. Отсутствие синхронизации ARIA-атрибутов

aria-selected должен соответствовать фактическому состоянию вкладки.


Архитектура доступных вкладок

Хорошая реализация вкладок включает несколько уровней:

Структура

  • семантическая разметка
  • роли ARIA

Навигация

  • управление клавиатурой
  • корректный порядок табуляции

Фокус

  • перемещение фокуса
  • изоляция скрытых элементов

Доступность

  • совместимость со скринридерами
  • предсказуемое поведение интерфейса

Библиотека ally.js предоставляет инструменты, позволяющие реализовать все эти механизмы в рамках одной архитектуры доступного пользовательского интерфейса.