Перейти к содержанию

Меню и раскрывающиеся блоки

Постоянно видимая навигация проста, но на небольшой странице может занимать много места. Раскрывающийся блок позволяет предложить её по запросу. При этом меняются два состояния: содержание видно или скрыто. Для читателя важно понять текущее состояние, открыть блок клавиатурой и продолжить движение по ссылкам без неожиданного переноса.

Продолжим хедер a11y-lab после урока о перестроении. Создадим отдельный учебный вариант раскрытия основной навигации. Это не модальное окно и не специальный командный интерфейс. За пределами блока остаются статья и другие ссылки; их управление не блокируется. Затем рассмотрим встроенный details для дополнительного пояснения внутри урока.

Кнопка открывает, ссылка переходит

Замените только основную навигацию внутри .site-header следующим фрагментом. Ссылка с названием сайта, специальный переход к содержимому и остальная страница остаются прежними:

<button id="course-toggle" type="button"
        aria-controls="course-menu" aria-expanded="true" hidden>
  Разделы сайта
</button>
<nav id="course-menu" aria-label="Основная">
  <a href="/catalog/">Каталог</a>
  <a href="/catalog/legacy/">Прежние уроки</a>
</nav>

Кнопка запускает действие в текущей странице; ссылки внутри блока открывают адреса. type="button" явно отделяет её от отправки формы, даже если оболочка позднее изменится. aria-controls указывает существующий идентификатор области. aria-expanded описывает раскрытие, а не выбор текущей страницы. Для текущего пункта, когда он существует, применяется отдельное состояние.

Изначально навигация видима, а кнопка скрыта. Это намеренная исходная модель: если скрипт не загрузится, ссылки останутся доступными. У скрытой кнопки записано состояние, соответствующее видимому блоку. При успешной инициализации скрипт включит управление и свернёт содержание. Не следует сначала скрывать ссылки только CSS, а затем надеяться на загрузку кода, который должен сделать их доступными.

Для защиты от существующего display: flex у навигации добавьте в CSS точное правило. Иначе авторский стиль может переопределить стандартное скрытие браузера:

#course-menu[hidden], #course-toggle[hidden] {
  display: none;
}

#course-toggle {
  padding: 0.65rem 1rem;
  font: inherit;
}

Правило относится только к нашему управлению. Установка hidden должна действительно убирать содержимое из визуального представления и обычного маршрута. Прозрачность opacity: 0 не решает ту же задачу: невидимые ссылки могут оставаться в последовательности и занимать место.

Одно изменение состояния

Добавьте этот фрагмент в конец существующего lesson.js. Переменная siteHeader уже определена в уроке 4 и повторно не объявляется:

const courseToggle = document.getElementById('course-toggle');
const courseMenu = document.getElementById('course-menu');

function setCourseOpen(open) {
  courseMenu.hidden = !open;
  courseToggle.setAttribute('aria-expanded', String(open));
  updateHeaderOffset();
}

setCourseOpen(false);
courseToggle.hidden = false;
updateHeaderOffset();

courseToggle.addEventListener('click', () => {
  setCourseOpen(courseMenu.hidden);
});

siteHeader.addEventListener('keydown', event => {
  const inside = courseMenu.contains(event.target) ||
    courseToggle.contains(event.target);
  if (event.key === 'Escape' && inside && !courseMenu.hidden) {
    setCourseOpen(false);
    courseToggle.focus();
  }
});

Функция синхронно меняет реальную видимость и описанное состояние. Это уменьшает риск, что кнопка объявляет раскрытый блок, пока CSS показывает закрытый. Затем уточняется компенсация хедера: раскрытие может изменить его высоту. Наблюдатель размеров из прошлого примера также остаётся, но прямой вызов делает зависимость явной.

Событие click у встроенной кнопки соответствует активации разными способами. Не нужно отдельно переключать состояние и на keydown Enter, и на click: один жест может привести к двум переключениям и вернуть исходный вид. Если выбран самодельный элемент вместо кнопки, придётся вручную воспроизводить больше поведения, поэтому он здесь не даёт преимущества.

После раскрытия фокус остаётся на кнопке. Следующий Tab ведёт к ссылкам, потому что они расположены дальше в HTML и больше не скрыты. Мы не выбираем первый пункт автоматически: читатель сам продолжает маршрут. Escape внутри управления сворачивает блок и возвращает фокус на кнопку; это предотвращает сохранение фокуса на только что скрытой ссылке.

Если блок сворачивается по иной причине, положение управления тоже необходимо определить. Например, при изменении ширины дизайнер может захотеть перейти от раскрывающейся версии к постоянно видимой. Простого удаления кнопки недостаточно, если именно на ней находится фокус. Нужно задать состояние новой компоновки и осмысленное продолжение маршрута. В нашем прототипе модель раскрытия одинакова при любой ширине, поэтому такого дополнительного перехода пока нет.

Рассмотрите также повторную активацию: открыли блок, закрыли его кнопкой, открыли снова. Ссылки должны возвращаться в тот же порядок, а не получать случайные новые значения tabindex. Добавлять их заново через HTML-строку незачем: изменение видимости сохраняет существующее содержимое и отношения. Если ссылки зависят от данных, загрузка и ошибка загрузки становятся отдельными состояниями, которые надо объяснить читателю.

Обычная навигация не становится menu

Внешне блок можно назвать меню, однако ему не обязательно назначать ARIA-роль menu. Такая роль соответствует отдельной модели управления со своими ожиданиями. Наш блок состоит из обычных ссылок сайта и кнопки раскрытия. Достаточно nav, понятного имени и состояния. Это различие поясняется в примере навигации APG.

Стрелочные клавиши в этой модели не заменяют Tab. Мы не перехватываем их для выбора ссылок и не блокируем обычную прокрутку страницы. Если проект действительно требует командного меню, нужно реализовать его целиком и объяснить другой маршрут. Добавление одной роли без соответствующего поведения делает интерфейс менее предсказуемым.

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

Дополнительное пояснение через details

Для небольшого замечания внутри раздела filter можно использовать встроенный раскрывающийся элемент. Вставьте следующий самостоятельный фрагмент после объяснения условия:

<details>
  <summary>Почему одинаковые имена встречаются в разных ветвях</summary>
  <p>Descendants ищет ниже исходного узла. Если Employee есть в разных
     частях документа, одного имени недостаточно для ограничения ветви.</p>
</details>

summary даёт название управления, а остальное содержимое является раскрываемым пояснением. Для этого простого варианта дополнительный JavaScript и собственное aria-expanded не нужны. Заголовок «Подробнее» был бы менее полезен: он не сообщает, какое уточнение откроется. У понятной подписи назначение сохраняется без чтения соседнего абзаца.

Главный смысл урока не следует прятать в закрытый блок только ради компактности. Если ограничение необходимо для корректного применения метода, оно должно присутствовать в основном объяснении. details подходит для дополнительного разъяснения, а не для скрытия обязательной части результата. Аналогично основные действия навигации должны оставаться доступными при отказе необязательного улучшения.

Полученный вариант имеет определённую исходную видимость, одно место изменения состояния и понятный путь после открытия. Для будущего наблюдения сравните активацию кнопки, переход по ссылке и Escape, а затем случай без скрипта. Паттерн Disclosure APG помогает оценить модель; конкретный пример требует проверки в выбранных сочетаниях браузера и вспомогательной технологии.