Поддержка экранных дикторов в веб-картографических библиотеках представляет сложную задачу из-за высокой динамичности canvas/WebGL-рендеринга и отсутствия нативной DOM-семантики для большинства географических объектов. В Mapbox GL JS основной рендеринг карты происходит через WebGL, что делает стандартные подходы доступности (ARIA-дерево DOM) неприменимыми к слоям карты напрямую.
Ключевая проблема заключается в том, что экранные дикторы (screen readers) взаимодействуют с DOM-структурой страницы, тогда как географические сущности — маркеры, полигоны, линии — визуализируются как пиксельные данные внутри canvas. Это требует построения параллельного слоя доступности, который синхронизируется с картой.
Для обеспечения базовой доступности используется концепция виртуального представления карты. Оно включает:
Основная цель — предоставить экранному диктору не визуальную, а смысловую модель карты.
Пример минимальной структуры:
<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:
movemoveendzoomrotatepitchЭти события позволяют формировать текстовое описание текущего состояния для экранного диктора.
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.
Подходы:
Пример списка объектов:
<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-роль:
role="application" — для сложных интерактивных
компонентов;aria-label — для описания назначения;<div id="map" role="application" aria-label="Географическая карта с интерактивными слоями"></div>
В некоторых сценариях допустимо использование
role="region" с дополнительным описанием, если карта не
является основным элементом интерфейса.
WebGL-рендеринг в Mapbox GL JS накладывает фундаментальные ограничения:
Поэтому доступность реализуется через:
Ключевой технический паттерн — двойное представление данных:
Пример синхронизации выделенного объекта:
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) требуют индивидуального подхода:
Каждый слой должен иметь текстовое представление, обновляемое при взаимодействии.
При потоковых обновлениях (например, движение транспорта) важно ограничивать частоту уведомлений:
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 доступность для экранных дикторов формируется как система:
Такая архитектура обеспечивает разделение визуального и семантического уровней, где экранный диктор взаимодействует исключительно с предсказуемым и структурированным DOM-представлением.