Contenteditable области

Библиотека Tabbable предназначена для определения всех элементов на странице, которые могут получать фокус через клавишу Tab. В обычном HTML это включает кнопки, ссылки, поля ввода, textarea и другие интерактивные элементы. Особый интерес представляет работа с элементами, имеющими атрибут contenteditable, так как они сами по себе не являются стандартными интерактивными элементами, но могут быть фокусируемыми и изменяемыми пользователем.

Элементы contenteditable позволяют редактировать текст прямо в DOM без использования дополнительных форм. Однако для корректной навигации через клавишу Tab требуется учитывать следующие особенности:

  • Фокусируемость: элемент с contenteditable="true" может быть получателем фокуса, если он имеет tabindex или является видимым. Без явно заданного tabindex браузеры по-разному обрабатывают Tab.
  • Внутренние элементы: внутри contenteditable могут находиться вложенные элементы (например, <b>, <i>, <span>). Tabbable обрабатывает только сам контейнер, но не отдельные вложенные узлы, если они не имеют собственного tabindex.

Определение tabbable элементов

Методология библиотеки основана на поиске всех элементов, которые удовлетворяют критериям:

  1. Наличие интерактивных тегов (a, button, input, textarea, select).
  2. Атрибут tabindex. Значение >= 0 делает элемент tabbable, -1 исключает из навигации через Tab.
  3. Видимость: элемент должен быть видимым (display !== 'none' и visibility !== 'hidden'), иначе он не считается tabbable.
  4. Специальная обработка contenteditable: элементы с contenteditable="true" включаются в список tabbable, если они видимы, даже при отсутствии tabindex.

Пример использования:

import tabbable from 'tabbable';

const editableDiv = document.querySelector('#editor');
const tabbableElements = tabbable(editableDiv);

console.log(tabbableElements);

В данном примере tabbable вернет массив элементов внутри #editor, которые могут принимать фокус, включая сам #editor, если он contenteditable.

Взаимодействие с tabindex

tabindex позволяет явно управлять порядком фокусировки. Для contenteditable это критически важно, если нужно, чтобы пользователь переходил к области редактирования после других интерактивных элементов. Правила:

  • tabindex="0" — элемент включается в естественный порядок табуляции.
  • tabindex="-1" — элемент исключается из табуляции, но остаётся фокусируемым программно (.focus()).
  • tabindex="1" и выше — задают приоритет в табуляции, что может нарушить естественный порядок, поэтому рекомендуется избегать больших положительных значений.
<div contenteditable="true" tabindex="0">Редактируемый текст</div>

В этом случае элемент будет доступен при переходе по клавише Tab.

Обработка вложенных элементов и кастомная фильтрация

Для сложных contenteditable блоков может потребоваться исключение некоторых внутренних элементов из tabbable списка или наоборот — включение элементов, которые по умолчанию не считаются фокусируемыми. Библиотека позволяет использовать кастомные фильтры:

const tabbableElements = tabbable('#editor', {
  includeContainer: true,
  getShadowRoot: el => el.shadowRoot, // поддержка shadow DOM
});

Параметр includeContainer обеспечивает включение самого контейнера contenteditable в список tabbable. Это важно, когда навигация должна перейти непосредственно к редактируемому блоку, а не только к вложенным интерактивным элементам.

Особенности браузеров

Разные браузеры по-разному обрабатывают contenteditable:

  • В Chrome и Firefox элементы без tabindex часто получают фокус, но Safari может игнорировать их.
  • Внутренние вложенные блоки (например <span> или <div> внутри contenteditable) по умолчанию не tabbable, что может вызвать проблемы при навигации.

Использование tabbable стандартизирует поведение, гарантируя единый результат в разных браузерах.

Практические рекомендации

  1. Всегда задавать tabindex="0" для элементов contenteditable, если требуется последовательная навигация.
  2. Использовать опцию includeContainer при вызове tabbable, чтобы включить контейнер в список элементов для фокусировки.
  3. Проверять результат функции tabbable при динамическом создании контента, особенно если внутренние элементы добавляются через JS.
  4. Для вложенных интерактивных элементов внутри contenteditable (например, кнопок или ссылок) убедиться, что они имеют корректный tabindex, чтобы не нарушить последовательность табуляции.

Интеграция с другими библиотеками

Tabbable может использоваться вместе с библиотеками управления фокусом, модальными окнами и редакторами WYSIWYG. В таких случаях важно:

  • Вызывать tabbable после рендера контента, чтобы получить актуальный список элементов.
  • Обрабатывать события focus и keydown для кастомной логики переключения табуляции внутри сложных contenteditable блоков.
  • Использовать Shadow DOM и слоты аккуратно, так как tabbable не проходит внутрь теневых корней без явного указания функции getShadowRoot.

Пример расширенного использования

import tabbable from 'tabbable';

const editorContainer = document.querySelector('#editor');

function focusNextEditable() {
  const elements = tabbable(editorContainer, { includeContainer: true });
  const activeIndex = elements.indexOf(document.activeElement);
  const nextIndex = (activeIndex + 1) % elements.length;
  elements[nextIndex].focus();
}

document.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    e.preventDefault();
    focusNextEditable();
  }
});

В этом примере реализована кастомная логика навигации через Tab внутри contenteditable, обеспечивая циклическое перемещение фокуса по всем tabbable элементам, включая сам редактируемый контейнер.