ARIA атрибуты

ARIA (Accessible Rich Internet Applications) представляет собой набор атрибутов, расширяющих семантику HTML для вспомогательных технологий. В контексте интерактивных карт на основе WebGL, таких как MapLibre GL JS, ARIA становится критическим слоем между графическим рендерингом и программной интерпретацией интерфейса.

Карты в браузере представляют собой сложный Canvas/WebGL-контент, который по своей природе не имеет семантики DOM. Без дополнительного описания элементы управления, маркеры, всплывающие окна и слои остаются «невидимыми» для экранных дикторов. ARIA-атрибуты компенсируют этот разрыв.

Ключевая особенность MapLibre GL JS — отделение визуального слоя (WebGL) от интерактивного DOM-слоя управления, что требует явного описания всех доступных пользователю действий.


Базовая доступность контейнера карты

Корневой контейнер карты в MapLibre GL JS обычно представляет собой <div> с вложенным <canvas>. Сам canvas не содержит семантики, поэтому ARIA-атрибуты применяются на уровне контейнера.

Типичная конфигурация:

<div id="map"
     role="application"
     aria-label="Интерактивная карта"
     tabindex="0"></div>

Разбор ключевых атрибутов:

  • role=“application” Указывает вспомогательным технологиям, что элемент содержит сложное интерактивное приложение. Это отключает часть стандартной навигации по документу.

  • aria-label Определяет текстовое описание карты. Без него экранный диктор не сможет интерпретировать назначение области.

  • tabindex=“0” Делает карту фокусируемой, что позволяет управлять клавиатурной навигацией.


ARIA и жизненный цикл MapLibre GL JS

При инициализации карты создаётся WebGL-контекст, но DOM-структура остаётся минимальной. Это означает, что доступность должна быть добавлена программно:

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [0, 0],
  zoom: 2
});

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

const container = map.getContainer();

container.setAttribute('role', 'application');
container.setAttribute('aria-label', 'Географическая карта мира');
container.setAttribute('tabindex', '0');

MapLibre GL JS не навязывает ARIA-структуру, поэтому ответственность за семантику лежит на разработчике.


Клавиатурная навигация и ARIA-связь

MapLibre GL JS поддерживает навигацию через клавиатуру, но её доступность должна быть описана через ARIA-связи.

Основные взаимодействия:

  • стрелки — перемещение по карте
  • +/- — масштабирование
  • Shift + drag — наклон (в 3D режимах)

Для описания можно использовать:

<div id="map"
     role="application"
     aria-label="Карта. Управление стрелками для перемещения, плюс и минус для масштаба"
     aria-describedby="map-help">
</div>

<div id="map-help" hidden>
  Используются стрелки для перемещения карты, клавиши плюс и минус для изменения масштаба.
</div>

aria-describedby связывает карту с расширенным описанием поведения, которое может быть озвучено экранным диктором.


ARIA для пользовательских контролов MapLibre GL JS

MapLibre GL JS позволяет добавлять кастомные контролы через интерфейс IControl. Каждый такой элемент должен иметь корректную семантику.

Пример структуры кнопки управления масштабом:

class ZoomControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';

    this.zoomInButton = document.createElement('button');
    this.zoomInButton.type = 'button';
    this.zoomInButton.innerHTML = '+';
    this.zoomInButton.setAttribute('aria-label', 'Приблизить карту');

    this.zoomOutButton = document.createElement('button');
    this.zoomOutButton.type = 'button';
    this.zoomOutButton.innerHTML = '−';
    this.zoomOutButton.setAttribute('aria-label', 'Отдалить карту');

    this.container.appendChild(this.zoomInButton);
    this.container.appendChild(this.zoomOutButton);

    this.zoomInButton.addEventListener('click', () => map.zoomIn());
    this.zoomOutButton.addEventListener('click', () => map.zoomOut());

    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = undefined;
  }
}

ARIA-аспекты:

  • aria-label заменяет визуальную иконографику
  • кнопки обязаны иметь type="button" для предотвращения сабмита форм
  • фокусируемость обеспечивается автоматически, но должна быть визуально различима

Маркеры и ARIA-атрибуты

Маркеры в MapLibre GL JS часто реализуются как DOM-элементы, что делает их основными кандидатами для ARIA-описания.

const el = document.createElement('div');
el.className = 'marker';

el.setAttribute('role', 'button');
el.setAttribute('tabindex', '0');
el.setAttribute('aria-label', 'Маркер: офис компании');

Добавление интерактивности:

el.addEventListener('click', () => {
  new maplibregl.Popup()
    .setLngLat([30, 50])
    .setHTML('<strong>Офис</strong>')
    .addTo(map);
});

Семантическая модель маркера:

  • role=“button” — маркер воспринимается как интерактивный элемент
  • tabindex=“0” — включение в последовательность навигации
  • aria-label — описание точки интереса

ARIA в всплывающих окнах (Popup)

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

const popup = new maplibregl.Popup({
  closeButton: true,
  closeOnClick: true
})
.setLngLat([30, 50])
.setHTML('<div role="dialog" aria-label="Информация о точке">Описание объекта</div>')
.addTo(map);

Важные моменты:

  • role=“dialog” определяет модальное поведение
  • aria-label задаёт заголовок контекста
  • фокус должен перемещаться внутрь popup при открытии

Дополнительная обработка фокуса:

popup.getElement().setAttribute('tabindex', '-1');
popup.getElement().focus();

ARIA для слоёв карты (layers)

Слои в MapLibre GL JS являются визуальной абстракцией и не имеют DOM-представления. Однако их состояние может быть описано через ARIA-метки в пользовательском интерфейсе управления слоями.

Пример панели слоёв:

<div role="region" aria-label="Панель управления слоями">
  <button aria-pressed="true">Дороги</button>
  <button aria-pressed="false">Здания</button>
</div>

aria-pressed используется для переключаемых состояний:

  • true — слой включён
  • false — слой отключён

При интеграции с MapLibre:

function toggleLayer(id, visible) {
  map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none');
}

Live regions и динамические обновления карты

MapLibre GL JS активно обновляет содержимое карты: перемещение, изменение масштаба, появление объектов. Эти изменения должны быть частично отражены через ARIA live regions.

<div aria-live="polite" id="map-status"></div>

Обновление состояния:

map.on('moveend', () => {
  const center = map.getCenter();
  document.getElementById('map-status').textContent =
    `Карта перемещена. Центр: ${center.lng.toFixed(2)}, ${center.lat.toFixed(2)}`;
});

aria-live=“polite” обеспечивает ненавязчивое обновление информации для экранных дикторов.


Фокус-менеджмент и ARIA взаимодействия

Фокусировка является критическим аспектом доступности карт. MapLibre GL JS не управляет фокусом автоматически, поэтому требуется явное управление.

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

  • фокус на контейнер карты при активации
  • возврат фокуса после закрытия popup
  • предотвращение «утери» фокуса при взаимодействии с WebGL

Пример возврата фокуса:

const previousFocus = document.activeElement;

popup.on('close', () => {
  if (previousFocus) {
    previousFocus.focus();
  }
});

ARIA-ошибки и типичные проблемы интеграции

1. Отсутствие role на контейнере карты Без role=“application” или альтернативного описания карта воспринимается как пустой блок.

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

3. Потеря фокуса при взаимодействии с WebGL Canvas не сохраняет семантический фокус, требуется внешнее управление.

4. Отсутствие описания динамики Перемещение карты без aria-live делает изменения состояния «немыми».

5. Неправильное использование role=“application” Избыточное применение этого role может ухудшить навигацию по документу, если интерфейс карты не требует сложного взаимодействия.


Интеграция ARIA с UI-слоем MapLibre GL JS

MapLibre GL JS предоставляет стандартные элементы управления (NavigationControl, ScaleControl, FullscreenControl). Эти элементы уже содержат базовую семантику, но при кастомизации требуется её сохранение.

Пример расширения стандартного контролла:

class CustomScaleControl extends maplibregl.ScaleControl {
  onAdd(map) {
    const container = super.onAdd(map);
    container.setAttribute('aria-label', 'Масштаб карты');
    return container;
  }
}

Семантическое проектирование интерфейса карты

При проектировании интерфейсов на MapLibre GL JS ARIA рассматривается не как дополнение, а как слой описания поведения:

  • карта = приложение (application)
  • контролы = кнопки и панели управления
  • маркеры = интерактивные элементы (button/link)
  • popups = диалоги (dialog)
  • панели = регионы (region)
  • состояние карты = live region

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