Svelte actions

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


Установка и подключение

Для использования Tabbable в проекте на JavaScript или Svelte необходимо установить пакет через npm:

npm install tabbable

Импортируется библиотека следующим образом:

import { tabbable } from 'tabbable';

В Svelte можно использовать этот импорт прямо внутри скрипта компонента:

<script>
  import { tabbable } from 'tabbable';
</script>

Основная функциональность

Метод tabbable(container, options) возвращает массив элементов, которые могут получить фокус с помощью клавиши Tab.

Сигнатура метода:

tabbable(container: HTMLElement, options?: { includeContainer?: boolean, strategy?: 'strict' | 'all' }): HTMLElement[]

Параметры:

  • container — родительский элемент, внутри которого производится поиск tabbable-элементов.

  • options.includeContainer — если true, сам контейнер также проверяется на возможность получения фокуса.

  • options.strategy — стратегия поиска:

    • 'strict' — только элементы, полностью соответствующие стандарту фокусируемых элементов.
    • 'all' — включает элементы, которые могут быть фокусируемыми через программный фокус, но не обязательно через Tab.

Возвращаемое значение: массив элементов типа HTMLElement.


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

Элементы считаются tabbable, если они соответствуют следующим условиям:

  1. Элемент видим на странице (display не none, visibility не hidden, opacity не 0 при родительских ограничениях).
  2. Элемент не заблокирован (disabled !== true).
  3. Элемент имеет атрибуты, позволяющие фокусироваться (tabindex ≥ 0, элементы <a> с href, <button>, <input>, <textarea>, <select>).
  4. Учет родительских контекстов с overflow: hidden или clip-path, которые могут ограничивать видимость элемента.

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

const container = document.querySelector('#form');
const focusableElements = tabbable(container);

focusableElements.forEach(el => {
  console.log(el.tagName, el.tabIndex);
});

Использование с Svelte actions

Svelte actions предоставляют удобный способ связывать поведение DOM-элементов с жизненным циклом компонента. Tabbable можно использовать через action для автоматического получения или управления tabbable-элементами.

Пример action для выделения первого tabbable-элемента внутри контейнера:

<script>
  import { tabbable } from 'tabbable';

  function focusFirstTabbable(node) {
    const elements = tabbable(node);
    if (elements.length > 0) {
      elements[0].focus();
    }

    return {
      destroy() {
        // очистка при удалении элемента, если необходимо
      }
    };
  }
</script>

<div use:focusFirstTabbable>
  <input type="text" />
  <button>Отправить</button>
  <a href="#">Ссылка</a>
</div>

В этом примере:

  • use:focusFirstTabbable — Svelte action, которая активируется при монтировании элемента.
  • Функция получает массив tabbable-элементов через tabbable(node) и устанавливает фокус на первый.

Управление tabindex

Tabbable учитывает значение атрибута tabindex, что позволяет контролировать порядок фокусировки:

  • tabindex="0" — элемент включается в естественный порядок табуляции.
  • tabindex="-1" — элемент может быть сфокусирован программно, но пропускается при Tab.
  • Положительные значения tabindex — задают явный порядок табуляции, приоритет перед 0.

Пример динамического управления tabindex:

<script>
  import { tabbable } from 'tabbable';
  
  let container;

  function disableTabbable() {
    tabbable(container).forEach(el => el.tabIndex = -1);
  }

  function enableTabbable() {
    tabbable(container).forEach(el => el.tabIndex = 0);
  }
</script>

<div bind:this={container}>
  <input type="text" />
  <button>Кнопка</button>
</div>

<button on:click={disableTabbable}>Отключить Tab</button>
<button on:click={enableTabbable}>Включить Tab</button>

Стратегии поиска элементов

Tabbable предоставляет гибкие стратегии для поиска:

  • strict — учитывает только реально доступные для Tab элементы. Используется для реализации навигации по клавиатуре.
  • all — включает элементы, которые можно программно сфокусировать. Полезно для случаев, когда требуется установить фокус через JavaScript без участия Tab.

Пример:

const allFocusable = tabbable(container, { strategy: 'all' });

Комбинация с кастомными компонентами

В Svelte-компонентах с модальными окнами, табами или аккордеонами Tabbable позволяет:

  • Ограничивать фокус внутри модального окна (focus trap).
  • Управлять порядком фокусировки при динамическом отображении контента.
  • Избегать потери фокуса при обновлении DOM.

Пример реализации фокус-трапа в Svelte:

<script>
  import { tabbable } from 'tabbable';

  let modal;

  function trapFocus(node) {
    const elements = tabbable(node);
    let first = elements[0];
    let last = elements[elements.length - 1];

    function handleKey(event) {
      if (event.key === 'Tab') {
        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      }
    }

    node.addEventListener('keydown', handleKey);

    return {
      destroy() {
        node.removeEventListener('keydown', handleKey);
      }
    };
  }
</script>

<div bind:this={modal} use:trapFocus>
  <input type="text" />
  <button>Закрыть</button>
</div>

В этом коде:

  • Фокус не выходит за пределы модального окна.
  • Shift+Tab и Tab корректно циклируют фокус между первыми и последними tabbable-элементами.

Особенности работы с динамическим контентом

При изменении DOM (добавлении или удалении элементов) важно повторно вычислять tabbable-элементы, так как массив, возвращаемый функцией tabbable, не обновляется автоматически. В Svelte это удобно реализовать через реактивные выражения:

<script>
  import { tabbable } from 'tabbable';

  let container;
  $: focusable = tabbable(container);
</script>

<div bind:this={container}>
  {#each items as item}
    <button>{item}</button>
  {/each}
</div>

Каждое изменение массива items автоматически обновит список tabbable-элементов.


Оптимизация и производительность

  • Для больших DOM-структур рекомендуется ограничивать контейнер поиска.
  • Использование strategy: 'strict' снижает количество проверок, ускоряя вычисление tabbable-элементов.
  • Можно кешировать результаты поиска, если DOM не изменяется часто.

Эта библиотека является базовым инструментом для контроля фокусировки в интерактивных компонентах, обеспечивая доступность и предсказуемое поведение клавиатурной навигации в приложениях на Svelte и чистом JavaScript.