Навигация с клавиатуры

Визуализации на основе Nivo строятся поверх React и SVG/Canvas-рендеринга, что требует отдельного слоя для обеспечения полноценной клавиатурной доступности. По умолчанию графики ориентированы на визуальное взаимодействие (мышь, hover, tooltips), поэтому поведение с клавиатуры реализуется через комбинацию встроенных механизмов доступности, ARIA-атрибутов и пользовательских обработчиков событий.

Ключевой принцип при работе с клавиатурной навигацией в графиках — перенос логики взаимодействия с визуальных элементов на семантические фокусируемые узлы. Это означает, что каждый интерактивный элемент (бар, точка, сектор, узел графа) должен быть доступен через tab-последовательность или альтернативные механизмы фокусировки.


Фокусируемые элементы и tabIndex

В Nivo большинство компонентов не являются фокусируемыми по умолчанию, поскольку SVG-элементы вроде <rect> или <path> не входят в стандартный поток клавиатурной навигации. Для решения используется:

  • tabIndex={0} для включения элемента в порядок фокусировки
  • tabIndex={-1} для программного управления фокусом без участия Tab

Пример настройки фокусируемых баров:

<Bar
    data={data}
    keys={['value']}
    indexBy="id"
    margin={{ top: 20, right: 20, bottom: 40, left: 40 }}
    layers={[
        ({ bars }) =>
            bars.map(bar => (
                <rect
                    key={bar.key}
                    x={bar.x}
                    y={bar.y}
                    width={bar.width}
                    height={bar.height}
                    tabIndex={0}
                    role="button"
                    aria-label={`Значение ${bar.data.value}`}
                    onFo cus={() => handleFocus(bar.data)}
                />
            ))
    ]}
/>

Фокусируемые элементы должны сопровождаться корректными ARIA-атрибутами, чтобы скринридеры могли интерпретировать их как интерактивные объекты.


Обработка клавиатурных событий

После добавления фокуса необходимо определить поведение клавиш. Чаще всего используются:

  • Enter и Space — активация элемента
  • стрелки — перемещение между элементами
  • Esc — сброс выделения или выход из режима взаимодействия

Типовой обработчик:

const handleKeyDown = (event, datum) => {
    switch (event.key) {
        case 'Enter':
        case ' ':
            selectDatum(datum);
            break;
        case 'Escape':
            clearSelection();
            break;
        default:
            break;
    }
};

Интеграция в слой рендеринга:

<rect
    tabIndex={0}
    onKeyD own={(e) => handleKeyDown(e, bar.data)}
/>

Навигация между элементами графика

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

Подходы:

1. Естественный tab order Подходит для простых графиков с ограниченным числом элементов. Элементы идут в DOM-порядке.

2. Ручное управление фокусом Используется при необходимости логической навигации (например, слева направо по оси X).

const focusNext = (currentIndex, items) => {
    const next = items[currentIndex + 1];
    if (next?.ref?.current) {
        next.ref.current.focus();
    }
};

3. Виртуальная навигация Применяется при больших наборах данных. Фокус остается на одном элементе, а активный индекс изменяется стрелками.


Управление состоянием выделения через клавиатуру

Клавиатурная навигация тесно связана с состоянием выделения элементов. В Nivo это часто реализуется через внешний state:

const [activeId, setActiveId] = useState(null);

const onKeyD own = (e, item) => {
    if (e.key === 'Enter') {
        setActiveId(item.id);
    }
};

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

  • подсветки
  • отображения tooltip
  • синхронизации с легендой
  • обновления вспомогательных панелей
fill: d => (d.id === activeId ? '#ff6600' : '#ccc')

Доступность через ARIA-атрибуты

SVG-графики не обладают семантикой HTML, поэтому ARIA становится ключевым инструментом.

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

  • role="img" для контейнера графика
  • role="button" для интерактивных элементов
  • aria-label для описания данных
  • aria-selected для выделенных элементов

Пример:

<svg role="img" aria-label="Гистограмма продаж по месяцам">
    <rect
        role="button"
        aria-label="Январь: 120"
        aria-selected={isActive}
    />
</svg>

Важно избегать перегрузки aria-label избыточной информацией — она должна быть краткой и информативной.


Синхронизация клавиатуры и tooltip-системы

В Nivo tooltip обычно активируется через hover, но для клавиатурного взаимодействия требуется явная синхронизация с focus-событиями.

const [tooltip, setTooltip] = useState(null);

const onFo cus = (datum) => {
    setTooltip({
        x: datum.x,
        y: datum.y,
        content: datum.value
    });
};
onFo cus={() => onFocus(bar.data)}
onB lur={() => setTooltip(null)}

Таким образом tooltip становится универсальным механизмом для всех типов ввода.


Навигация в линейных и точечных графиках

В scatter plot и line chart часто используется концепция «активной точки».

Поведение:

  • стрелка вправо/влево — переход по X-оси
  • стрелка вверх/вниз — переход по Y-значениям или сериям
  • Enter — фиксация точки
const handleArrowNavigation = (e) => {
    if (e.key === 'ArrowRight') {
        setIndex(i => Math.min(i + 1, data.length - 1));
    }
    if (e.key === 'ArrowLeft') {
        setIndex(i => Math.max(i - 1, 0));
    }
};

Работа с несколькими сериями данных

При наличии нескольких наборов данных важно различать навигацию внутри серии и между сериями.

Подход:

  • горизонтальные стрелки — внутри серии
  • вертикальные стрелки — переключение серии
const move = (key) => {
    switch (key) {
        case 'ArrowUp':
            setSeries(s => Math.max(s - 1, 0));
            break;
        case 'ArrowDown':
            setSeries(s => Math.min(s + 1, seriesCount - 1));
            break;
    }
};

Управление фокусом при перерисовке графика

Nivo часто пересчитывает layout при изменении данных. Это может приводить к потере фокуса.

Решение:

  • хранить id активного элемента
  • после обновления данных восстанавливать фокус через useEffect
useEffect(() => {
    const el = refs[activeId]?.current;
    if (el) el.focus();
}, [data]);

Кастомные слои и расширение клавиатурной логики

Nivo позволяет добавлять кастомные слои (layers), что используется для внедрения общей логики навигации.

Пример слоя:

const KeyboardLayer = ({ bars }) => {
    return bars.map(bar => (
        <rect
            key={bar.key}
            tabIndex={0}
            onKeyD own={(e) => handleKeyDown(e, bar.data)}
        />
    ));
};

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


Ограничения и типичные ошибки

  • отсутствие tabIndex приводит к полной недоступности с клавиатуры
  • использование только hover-событий блокирует доступ скринридеров
  • слишком длинные aria-label ухудшают восприятие
  • отсутствие синхронизации state и фокуса вызывает «прыгающий» интерфейс
  • попытка навигации без учета структуры данных делает UX непредсказуемым

Оптимизация поведения на больших наборах данных

При тысячах точек или баров полная клавиатурная навигация становится неэффективной. Используются стратегии:

  • виртуализация элементов
  • навигация по агрегированным группам
  • переключение «режимов фокуса» (например, месяц → день → значение)
const [mode, setMode] = useState('aggregate');

В агрегированном режиме навигация работает по укрупненным блокам, снижая нагрузку на DOM и упрощая восприятие с клавиатуры.