Screen readers поддержка

Поддержка экранных дикторов в веб-картографических библиотеках представляет сложную задачу из-за высокой динамичности canvas/WebGL-рендеринга и отсутствия нативной DOM-семантики для большинства географических объектов. В Mapbox GL JS основной рендеринг карты происходит через WebGL, что делает стандартные подходы доступности (ARIA-дерево DOM) неприменимыми к слоям карты напрямую.

Ключевая проблема заключается в том, что экранные дикторы (screen readers) взаимодействуют с DOM-структурой страницы, тогда как географические сущности — маркеры, полигоны, линии — визуализируются как пиксельные данные внутри canvas. Это требует построения параллельного слоя доступности, который синхронизируется с картой.


Семантический слой поверх WebGL-карты

Для обеспечения базовой доступности используется концепция виртуального представления карты. Оно включает:

  • скрытую DOM-структуру с ARIA-ролями;
  • описания ключевых объектов карты;
  • обновляемые состояния центра карты, масштаба и выделенных объектов.

Основная цель — предоставить экранному диктору не визуальную, а смысловую модель карты.

Пример минимальной структуры:

<div id="map" role="application" aria-label="Интерактивная карта">
  <div id="map-sr-info" aria-live="polite" class="sr-only"></div>
</div>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Элемент aria-live="polite" используется для передачи изменений состояния карты без необходимости фокусировки.


Отслеживание состояния карты

В Mapbox GL JS доступ к состоянию карты осуществляется через события API:

  • move
  • moveend
  • zoom
  • rotate
  • pitch

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

map.on('moveend', () => {
  const center = map.getCenter();
  const zoom = map.getZoom();

  const message = `Карта обновлена. Центр: широта ${center.lat.toFixed(3)}, долгота ${center.lng.toFixed(3)}. Масштаб ${zoom.toFixed(1)}.`;

  document.getElementById('map-sr-info').textContent = message;
});

Такой подход обеспечивает непрерывное обновление смыслового состояния без участия визуального слоя.


Доступность элементов управления картой

Контролы масштабирования и вращения должны быть представлены как стандартные HTML-элементы с корректными ARIA-атрибутами:

  • aria-label для описания действия;
  • button вместо кастомных div-элементов;
  • поддержка клавиатурной навигации.

Пример:

<button aria-label="Увеличить масштаб карты" id="zoom-in">+</button>
<button aria-label="Уменьшить масштаб карты" id="zoom-out">−</button>
document.getElementById('zoom-in').addEventListener('click', () => {
  map.zoomIn();
});

document.getElementById('zoom-out').addEventListener('click', () => {
  map.zoomOut();
});

Экранный диктор получает доступ к действиям через стандартную семантику кнопок, не требуя анализа canvas.


Описание географических объектов

Маркерные слои и POI (points of interest) требуют отдельного представления для экранных дикторов. Визуальные точки на карте не имеют семантики, поэтому необходимо дублирование информации в DOM.

Подходы:

  • генерация скрытых списков объектов;
  • привязка ARIA-label к интерактивным элементам;
  • создание фокусируемых элементов при взаимодействии.

Пример списка объектов:

<ul id="poi-list">
  <li tabindex="0" aria-label="Ресторан: Центральное кафе, рейтинг 4.5">
    Центральное кафе
  </li>
</ul>

При синхронизации с картой:

map.on('click', 'restaurants-layer', (e) => {
  const feature = e.features[0];

  const message = `Выбран объект: ${feature.properties.name}, категория: ресторан.`;

  document.getElementById('map-sr-info').textContent = message;
});

Управление фокусом и клавиатурная навигация

Экранные дикторы тесно связаны с клавиатурной моделью навигации. В WebGL-картах фокус должен быть явно контролируемым.

Основные принципы:

  • карта получает tabindex="0";
  • стрелки управляют перемещением;
  • клавиши + и - изменяют масштаб;
  • Escape возвращает фокус в UI.
map.getCanvas().setAttribute('tabindex', '0');

map.getCanvas().addEventListener('keydown', (e) => {
  switch (e.key) {
    case '+':
      map.zoomIn();
      break;
    case '-':
      map.zoomOut();
      break;
    case 'ArrowUp':
      map.panBy([0, -100]);
      break;
    case 'ArrowDown':
      map.panBy([0, 100]);
      break;
  }
});

Такой подход позволяет экранному диктору корректно отслеживать активный элемент.


ARIA-роль и структура контейнера карты

Корневой контейнер карты должен иметь корректную ARIA-роль:

  • role="application" — для сложных интерактивных компонентов;
  • aria-label — для описания назначения;
  • избегание избыточной вложенности ролей.
<div id="map" role="application" aria-label="Географическая карта с интерактивными слоями"></div>

В некоторых сценариях допустимо использование role="region" с дополнительным описанием, если карта не является основным элементом интерфейса.


Проблема WebGL и ограничения доступности

WebGL-рендеринг в Mapbox GL JS накладывает фундаментальные ограничения:

  • отсутствует доступ к индивидуальным пикселям через DOM;
  • невозможно автоматически интерпретировать геометрию слоёв;
  • экранный диктор не может «видеть» карту.

Поэтому доступность реализуется через:

  • дублирование данных;
  • семантические описания;
  • управляемые события состояния.

Синхронизация визуального и семантического состояния

Ключевой технический паттерн — двойное представление данных:

  1. визуальный слой (WebGL);
  2. доступный слой (ARIA/DOM).

Пример синхронизации выделенного объекта:

function announceFeature(feature) {
  const description = `
    Объект: ${feature.properties.name}.
    Тип: ${feature.properties.type}.
    Координаты: ${feature.geometry.coordinates[1]}, ${feature.geometry.coordinates[0]}.
  `;

  document.getElementById('map-sr-info').textContent = description;
}
map.on('mouseenter', 'points-layer', (e) => {
  map.getCanvas().style.cursor = 'pointer';
  announceFeature(e.features[0]);
});

Поддержка фокусируемых интерактивных объектов

Для сложных сценариев (например, маршруты или зоны) применяются виртуальные элементы:

  • скрытые кнопки для каждого объекта;
  • программное управление фокусом;
  • синхронизация с координатами карты.
const button = document.createElement('button');
button.textContent = 'Выбрать маршрут A';
button.setAttribute('aria-label', 'Маршрут A');
button.addEventListener('click', () => {
  map.flyTo({ center: routeCenter, zoom: 12 });
});

Практика текстового описания карты

Экранные дикторы не работают с визуальной сложностью, поэтому требуется генерация структурированных описаний:

  • текущий регион;
  • уровень масштаба;
  • активные слои;
  • выбранные объекты.

Пример формата сообщения:

function buildMapDescription() {
  const bounds = map.getBounds();
  const zoom = map.getZoom();

  return `
    Отображается область: север ${bounds.getNorth().toFixed(2)},
    юг ${bounds.getSouth().toFixed(2)},
    запад ${bounds.getWest().toFixed(2)},
    восток ${bounds.getEast().toFixed(2)}.
    Текущий масштаб ${zoom.toFixed(1)}.
  `;
}

Интеграция с системой слоёв

Слои в Mapbox GL JS (symbol, line, fill, circle) требуют индивидуального подхода:

  • symbol layers → списки объектов;
  • line layers → маршруты с описанием точек;
  • fill layers → зоны с географическими границами.

Каждый слой должен иметь текстовое представление, обновляемое при взаимодействии.


Обработка динамических обновлений данных

При потоковых обновлениях (например, движение транспорта) важно ограничивать частоту уведомлений:

  • debounce обновлений;
  • приоритизация значимых событий;
  • агрегирование сообщений.
let timeout;

map.on('data', () => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    document.getElementById('map-sr-info').textContent =
      'Данные карты обновлены.';
  }, 500);
});

Совместимость с ассистивными технологиями

Экранные дикторы (NVDA, VoiceOver, JAWS) интерпретируют DOM-слой, поэтому необходимо:

  • избегать чрезмерных aria-live обновлений;
  • минимизировать шумовые сообщения;
  • поддерживать логическую структуру сообщений;
  • обеспечивать предсказуемость обновлений состояния.

Итоговая архитектурная модель доступности

В рамках Mapbox GL JS доступность для экранных дикторов формируется как система:

  • WebGL визуализация;
  • ARIA-слой состояния;
  • событийная синхронизация;
  • виртуальные элементы управления;
  • текстовая репрезентация геоданных.

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