Tab order

Поведение клавиши Tab в интерактивных веб-картах напрямую влияет на доступность интерфейса и предсказуемость навигации. В контексте Mapbox GL JS порядок фокусировки определяется сочетанием DOM-структуры, атрибутов tabindex, встроенных контролов и пользовательских компонентов, добавляемых поверх карты.

Карта в Mapbox фактически является сложным слоем DOM-элементов, где часть объектов управляется библиотекой, а часть создаётся разработчиком. Поэтому порядок перехода фокуса не является фиксированным и требует явного контроля.


Базовый фокус контейнера карты

Корневой контейнер карты создаётся как обычный div, который по умолчанию не участвует в табуляции:

<div id="map"></div>

Чтобы карта стала доступной для клавиатурной навигации, контейнеру задаётся tabindex:

const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v12'
});

map.getContainer().tabIndex = 0;

Значение tabindex = 0 означает:

  • элемент попадает в естественный порядок Tab
  • фокус устанавливается в соответствии с DOM-последовательностью
  • карта становится достижимой с клавиатуры

Без этого шага фокус никогда не попадёт внутрь карты, даже если внутри есть интерактивные элементы.


Внутренняя логика фокусируемых элементов

Внутри Mapbox GL JS существует несколько категорий элементов, влияющих на tab order:

Контролы карты

Элементы управления, такие как зум, компас и геолокация:

map.addControl(new mapboxgl.NavigationControl());

Каждая кнопка внутри контролов автоматически получает tabindex="0" и добавляется в общий поток фокуса после контейнера карты.

Порядок обычно выглядит так:

  1. контейнер карты
  2. кнопка zoom in
  3. кнопка zoom out
  4. кнопка rotation reset (если включена)

Этот порядок формируется DOM-порядком внутри control container.


Маркеры и DOM-элементы

Стандартные маркеры:

new mapboxgl.Marker()
  .setLngLat([30, 50])
  .addTo(map);

По умолчанию маркеры:

  • не являются фокусируемыми
  • не участвуют в tab order

Чтобы включить их в обход клавишей Tab:

const el = document.createElement('div');
el.tabIndex = 0;

new mapboxgl.Marker(el)
  .setLngLat([30, 50])
  .addTo(map);

После этого маркер становится частью последовательности фокуса и упорядочивается согласно DOM-позиции слоя маркеров.


Попапы и временные DOM-узлы

Popup-элементы создают временные контейнеры:

new mapboxgl.Popup()
  .setLngLat([30, 50])
  .setHTML('<button>Действие</button>')
  .addTo(map);

Фокус внутри popup зависит от структуры HTML:

  • все интерактивные элементы внутри становятся частью tab order
  • popup добавляется в конец DOM-контейнера карты
  • после закрытия фокус не возвращается автоматически без дополнительной логики

Принципы формирования tab order

Tab order внутри карты подчиняется общим правилам браузера, но усложняется из-за вложенности слоёв:

1. DOM-порядок как основной фактор

Последовательность:

map container
  controls
  markers
  popups

Фокус движется строго по DOM, а не по географическому положению объектов.


2. Влияние z-index не влияет на Tab

Несмотря на визуальное перекрытие слоёв:

  • z-index влияет только на отрисовку
  • tab order остаётся DOM-ориентированным

Это часто приводит к несоответствию визуального и логического порядка.


3. Динамические слои

При добавлении новых маркеров или контролов:

map.on('load', () => {
  addMarkers();
});

новые элементы вставляются в конец соответствующего контейнера, изменяя tab order без дополнительной сортировки.


Управление tabindex в пользовательских компонентах

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

class CustomControl {
  onAdd(map) {
    this.container = document.createElement('div');
    this.container.className = 'custom-control';

    const button = document.createElement('button');
    button.textContent = 'Action';

    this.container.appendChild(button);

    return this.container;
  }

  onRemove() {
    this.container.parentNode.removeChild(this.container);
  }
}

Чтобы контрол корректно участвовал в tab order:

this.container.tabIndex = 0;

или лучше:

button.tabIndex = 0;

Второй вариант предпочтительнее, поскольку:

  • фокус попадает на конкретное действие
  • не создаётся лишний шаг фокусировки на контейнере

Перехват и управление клавишей Tab

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

map.getContainer().addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    console.log('Tab внутри карты');
  }
});

Однако перехват Tab требует осторожности, поскольку:

  • может нарушить системную навигацию
  • усложняет доступность
  • требует ручной реализации логики переходов

Ограничение выхода фокуса из карты

Иногда требуется удерживать фокус внутри карты (modal-like behavior):

map.getContainer().addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    const focusable = map.getContainer().querySelectorAll(
      'button, [href], input, [tabindex="0"]'
    );

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    }

    if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
});

Такая логика создаёт циклический tab order внутри карты.


Слои WebGL и отсутствие фокусируемых объектов

Визуальные слои карты (raster, vector, fill, line, symbol) не являются DOM-элементами. Поэтому:

  • они не участвуют в tab order
  • не могут получать фокус напрямую
  • требуют дополнительного HTML-оверлея для интерактивности

Пример проблемы:

  • клик по объекту возможен
  • Tab-навигация к объекту невозможна

Решение — синхронизация слоя с DOM-маркером или скрытым контролом.


aria и согласованность табуляции

Хотя tab order и tabindex управляют последовательностью, доступность дополняется ARIA-атрибутами:

<button aria-label="Zoom in map"></button>

или:

button.setAttribute('aria-label', 'Open marker details');

Это влияет не на порядок Tab, а на читаемость интерфейса скринридерами, которые следуют тому же DOM-порядку, что и клавиатурная навигация.


Типичные проблемы порядка фокуса

Нарушение последовательности из-за динамических элементов

Добавление маркеров после загрузки может переместить их в конец tab order, что не соответствует логике карты.

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

Если tabindex задан только контейнеру карты, пользователю приходится вручную входить внутрь интерфейса.

Потеря фокуса при обновлении popup

Перерендер popup может сбрасывать активный элемент и выводить фокус за пределы карты.


Стратегии стабилизации tab order

Явное управление tabindex

  • назначение tabindex только интерактивным элементам
  • избегание tabindex > 0, чтобы не ломать естественный порядок

Контроль DOM-вставки

  • добавление элементов в предсказуемые контейнеры
  • отказ от случайной вставки в конец body

Синхронизация с состоянием карты

  • обновление tabindex при изменении слоёв
  • пересборка фокусируемых элементов после динамических операций

Поведение при множественных контролах

Если добавлено несколько контролов:

map.addControl(new mapboxgl.NavigationControl());
map.addControl(new mapboxgl.FullscreenControl());

tab order будет зависеть от порядка добавления:

  1. NavigationControl
  2. FullscreenControl

Изменение порядка добавления — единственный надёжный способ управления последовательностью без переопределения DOM.


Взаимодействие с внешними интерфейсами

При наличии панели рядом с картой:

<div class="sidebar">
  <button>Фильтр</button>
</div>

<div id="map"></div>

Tab order строится по DOM:

  1. sidebar кнопки
  2. контейнер карты
  3. элементы карты

Это создаёт необходимость учитывать структуру страницы целиком, а не только карту.