Визуализации на основе Nivo строятся поверх React и SVG/Canvas-рендеринга, что требует отдельного слоя для обеспечения полноценной клавиатурной доступности. По умолчанию графики ориентированы на визуальное взаимодействие (мышь, hover, tooltips), поэтому поведение с клавиатуры реализуется через комбинацию встроенных механизмов доступности, ARIA-атрибутов и пользовательских обработчиков событий.
Ключевой принцип при работе с клавиатурной навигацией в графиках —
перенос логики взаимодействия с визуальных элементов на
семантические фокусируемые узлы. Это означает, что каждый
интерактивный элемент (бар, точка, сектор, узел графа) должен быть
доступен через tab-последовательность или альтернативные
механизмы фокусировки.
В 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);
}
};
Активный элемент затем используется для:
fill: d => (d.id === activeId ? '#ff6600' : '#ccc')
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 избыточной
информацией — она должна быть краткой и информативной.
В 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 часто используется концепция «активной точки».
Поведение:
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 активного элементаuseEffectuseEffect(() => {
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 приводит к полной недоступности с
клавиатурыaria-label ухудшают восприятиеПри тысячах точек или баров полная клавиатурная навигация становится неэффективной. Используются стратегии:
const [mode, setMode] = useState('aggregate');
В агрегированном режиме навигация работает по укрупненным блокам, снижая нагрузку на DOM и упрощая восприятие с клавиатуры.