Поддержка клавиатурной навигации

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

По умолчанию Chart.js не предоставляет полноценную систему клавиатурной навигации. Canvas-элемент не содержит интерактивных HTML-узлов для отдельных точек, столбцов или сегментов диаграммы. Вся графика рендерится внутри одного <canvas>, поэтому разработчику необходимо самостоятельно реализовывать:

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

Проблема canvas и доступности

Canvas не является семантическим элементом интерфейса. Браузер воспринимает его как единый графический контейнер.

Пример:

<canvas id="chart"></canvas>

С точки зрения accessibility внутри canvas отсутствуют:

  • кнопки;
  • ссылки;
  • ячейки;
  • фокусируемые области;
  • описания данных.

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

Поэтому поддержка клавиатурной навигации строится вокруг нескольких механизмов:

  1. Фокусировка самого canvas.
  2. Управление активным элементом вручную.
  3. Реакция на клавиши.
  4. Визуальное выделение выбранного элемента.
  5. Озвучивание информации через ARIA.

Деление навигации на уровни

Поддержку клавиатуры удобно разделять на несколько уровней.

Базовый уровень

Позволяет:

  • перейти на график через Tab;
  • увидеть фокус;
  • активировать подсказки.

Средний уровень

Добавляет:

  • переключение между элементами;
  • движение стрелками;
  • выделение точек и столбцов.

Продвинутый уровень

Включает:

  • ARIA live regions;
  • синхронизацию с таблицей данных;
  • полноценную screen-reader поддержку;
  • управление несколькими datasets;
  • сложную логику навигации.

Добавление фокуса canvas

Canvas не получает фокус автоматически. Для поддержки клавиатуры необходимо добавить tabindex.

<canvas id="salesChart" tabindex="0"></canvas>

Теперь элемент можно выбрать клавишей Tab.


Стилизация фокуса

Без визуального отображения пользователь не поймёт, что график активен.

canvas:focus {
    outline: 3px solid #2563eb;
    outline-offset: 4px;
}

Такой подход особенно важен при работе с accessibility-аудитом.


Создание базового графика

<canvas id="salesChart" tabindex="0"></canvas>
const ctx = document.getElementById('salesChart');

const chart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Январь', 'Февраль', 'Март', 'Апрель'],
        datasets: [{
            label: 'Продажи',
            data: [12, 19, 8, 15],
            backgroundColor: '#3b82f6'
        }]
    },
    options: {
        responsive: true
    }
});

На этом этапе график уже можно выбрать клавишей Tab, но навигация между столбцами ещё отсутствует.


Хранение активного элемента

Для перемещения по данным требуется состояние текущего выбранного элемента.

let activeIndex = 0;

Навигация стрелками

Добавление обработчика клавиатуры:

ctx.addEventListener('keydown', (event) => {

    if (event.key === 'ArrowRight') {
        activeIndex++;
    }

    if (event.key === 'ArrowLeft') {
        activeIndex--;
    }

    if (activeIndex < 0) {
        activeIndex = 0;
    }

    if (activeIndex >= chart.data.labels.length) {
        activeIndex = chart.data.labels.length - 1;
    }

    updateActiveElement();
});

Выделение активного столбца

Chart.js позволяет программно активировать элементы.

function updateActiveElement() {

    chart.setActiveElements([
        {
            datasetIndex: 0,
            index: activeIndex
        }
    ]);

    chart.tooltip.setActiveElements([
        {
            datasetIndex: 0,
            index: activeIndex
        }
    ]);

    chart.update();
}

После этого стрелки клавиатуры начинают переключать столбцы диаграммы.


Принцип работы setActiveElements

Метод:

chart.setActiveElements()

эмулирует наведение мыши на элемент.

Это позволяет:

  • подсвечивать элементы;
  • показывать tooltip;
  • активировать hover-стили;
  • синхронизировать accessibility-состояния.

Поддержка Enter и Space

Часто диаграмма используется как интерактивный компонент.

Например:

  • открытие подробной статистики;
  • переход к отчёту;
  • фильтрация данных;
  • drill-down навигация.

Поддержка Enter:

ctx.addEventListener('keydown', (event) => {

    if (event.key === 'Enter') {

        const label = chart.data.labels[activeIndex];
        const value = chart.data.datasets[0].data[activeIndex];

        console.log(label, value);
    }
});

Поддержка Space:

if (event.key === ' ') {
    event.preventDefault();
}

preventDefault() предотвращает прокрутку страницы пробелом.


Озвучивание данных через ARIA

Screen reader должен получать информацию о выбранном элементе.

Создание live region:

<div id="chart-announcer" aria-live="polite"></div>

Обновление текста:

function announceValue() {

    const label = chart.data.labels[activeIndex];
    const value = chart.data.datasets[0].data[activeIndex];

    const announcer = document.getElementById('chart-announcer');

    announcer.textContent =
        `${label}: ${value}`;
}

Интеграция:

function updateActiveElement() {

    chart.setActiveElements([
        {
            datasetIndex: 0,
            index: activeIndex
        }
    ]);

    chart.update();

    announceValue();
}

Скрытие live region визуально

ARIA-область не обязана отображаться на экране.

#chart-announcer {
    position: absolute;
    left: -9999px;
}

Такой элемент остаётся доступным для screen reader.


Добавление ARIA-описания графика

Canvas поддерживает атрибуты доступности.

<canvas
    id="salesChart"
    tabindex="0"
    role="img"
    aria-label="График продаж по месяцам">
</canvas>

Использование aria-describedby

Иногда требуется более подробное описание.

<p id="chartDescription">
    Диаграмма показывает продажи за первые четыре месяца года.
</p>

<canvas
    id="salesChart"
    tabindex="0"
    aria-describedby="chartDescription">
</canvas>

Поддержка навигации по datasets

Если график содержит несколько наборов данных, навигация усложняется.

Пример:

datasets: [
    {
        label: '2024',
        data: [12, 19, 8, 15]
    },
    {
        label: '2025',
        data: [16, 22, 11, 18]
    }
]

Требуется хранить:

let activeDataset = 0;
let activeIndex = 0;

Переключение между наборами данных

Например:

  • стрелки влево/вправо — перемещение по индексам;
  • стрелки вверх/вниз — переключение datasets.
ctx.addEventListener('keydown', (event) => {

    switch (event.key) {

        case 'ArrowRight':
            activeIndex++;
            break;

        case 'ArrowLeft':
            activeIndex--;
            break;

        case 'ArrowUp':
            activeDataset--;
            break;

        case 'ArrowDown':
            activeDataset++;
            break;
    }

    normalizeIndexes();

    updateActiveElement();
});

Нормализация индексов

function normalizeIndexes() {

    const datasetsCount = chart.data.datasets.length;
    const itemsCount = chart.data.labels.length;

    if (activeDataset < 0) {
        activeDataset = 0;
    }

    if (activeDataset >= datasetsCount) {
        activeDataset = datasetsCount - 1;
    }

    if (activeIndex < 0) {
        activeIndex = 0;
    }

    if (activeIndex >= itemsCount) {
        activeIndex = itemsCount - 1;
    }
}

Полное обновление активного элемента

function updateActiveElement() {

    chart.setActiveElements([
        {
            datasetIndex: activeDataset,
            index: activeIndex
        }
    ]);

    chart.tooltip.setActiveElements([
        {
            datasetIndex: activeDataset,
            index: activeIndex
        }
    ]);

    chart.update();
}

Навигация в line chart

Линейные графики требуют особенно аккуратной реализации.

Причины:

  • маленькие точки;
  • большое количество данных;
  • плотное расположение элементов.

Пример:

type: 'line'

Навигация работает аналогично bar chart, но рекомендуется:

  • увеличивать pointRadius;
  • добавлять pointHoverRadius;
  • усиливать контраст активной точки.

Увеличение активной точки

elements: {
    point: {
        radius: 4,
        hoverRadius: 8
    }
}

Кастомное выделение активного элемента

Можно изменять стили динамически.

function highlightPoint(index) {

    chart.data.datasets[0].pointRadius =
        chart.data.datasets[0].data.map((_, i) => {
            return i === index ? 8 : 4;
        });

    chart.update();
}

Синхронизация с HTML-таблицей

Для accessibility рекомендуется предоставлять альтернативное табличное представление.

<table>
    <thead>
        <tr>
            <th>Месяц</th>
            <th>Продажи</th>
        </tr>
    </thead>

    <tbody>
        <tr>
            <td>Январь</td>
            <td>12</td>
        </tr>

        <tr>
            <td>Февраль</td>
            <td>19</td>
        </tr>
    </tbody>
</table>

Связь графика и таблицы

При перемещении по графику можно выделять строку таблицы.

function highlightTableRow(index) {

    const rows =
        document.querySelectorAll('tbody tr');

    rows.forEach(row => {
        row.classList.remove('active');
    });

    rows[index].classList.add('active');
}

Визуальное выделение строки

tbody tr.active {
    background: #dbeafe;
}

Поддержка Home и End

Дополнительные клавиши улучшают UX.

switch (event.key) {

    case 'Home':
        activeIndex = 0;
        break;

    case 'End':
        activeIndex =
            chart.data.labels.length - 1;
        break;
}

Циклическая навигация

Иногда удобнее переходить от последнего элемента к первому.

if (activeIndex < 0) {
    activeIndex =
        chart.data.labels.length - 1;
}

if (activeIndex >= chart.data.labels.length) {
    activeIndex = 0;
}

Поддержка Escape

Escape может использоваться для:

  • снятия выделения;
  • скрытия tooltip;
  • выхода из режима навигации.
if (event.key === 'Escape') {

    chart.setActiveElements([]);
    chart.update();
}

Работа с tooltip

Tooltip играет важную роль в keyboard navigation.

Настройки:

options: {
    plugins: {
        tooltip: {
            enabled: true
        }
    }
}

Принудительное отображение tooltip

chart.tooltip.setActiveElements([
    {
        datasetIndex: activeDataset,
        index: activeIndex
    }
]);

Accessibility-проблемы tooltip

Canvas tooltip не всегда читается screen reader.

Поэтому рекомендуется:

  • дублировать текст в live region;
  • использовать HTML tooltip;
  • выводить данные вне canvas.

HTML tooltip как альтернатива

Chart.js позволяет отключить встроенный tooltip.

plugins: {
    tooltip: {
        enabled: false,
        external: externalTooltipHandler
    }
}

Пример внешнего tooltip

function externalTooltipHandler(context) {

    const tooltip = context.tooltip;

    const element =
        document.getElementById('tooltip');

    if (tooltip.opacity === 0) {
        element.style.opacity = 0;
        return;
    }

    const point = tooltip.dataPoints[0];

    element.innerHTML =
        `${point.label}: ${point.raw}`;

    element.style.opacity = 1;
}

HTML tooltip и screen readers

HTML-элементы значительно лучше взаимодействуют с:

  • NVDA;
  • VoiceOver;
  • JAWS;
  • TalkBack.

Поддержка focus trap

Если график является частью сложного интерфейса, может потребоваться ограничение фокуса.

Например:

  • аналитическая панель;
  • модальное окно;
  • dashboard;
  • fullscreen chart mode.

Управление tabindex

Для сложных интерфейсов удобно динамически переключать tabindex.

canvas.tabIndex = 0;

Или:

canvas.tabIndex = -1;

Проверка видимости активного элемента

При горизонтальной прокрутке важно следить за областью видимости.

Например:

canvas.scrollIntoView({
    block: 'nearest',
    inline: 'nearest'
});

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

На планшетах и мобильных устройствах с внешней клавиатурой события работают аналогично desktop.

Основные события:

keydown
keyup

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

Keyboard navigation удобно выносить в отдельный Chart.js plugin.


Базовый accessibility plugin

const keyboardNavigationPlugin = {

    id: 'keyboardNavigation',

    afterInit(chart) {

        const canvas = chart.canvas;

        canvas.tabIndex = 0;

        let activeIndex = 0;

        canvas.addEventListener('keydown', (event) => {

            if (event.key === 'ArrowRight') {
                activeIndex++;
            }

            if (event.key === 'ArrowLeft') {
                activeIndex--;
            }

            const max =
                chart.data.labels.length - 1;

            if (activeIndex < 0) {
                activeIndex = 0;
            }

            if (activeIndex > max) {
                activeIndex = max;
            }

            chart.setActiveElements([
                {
                    datasetIndex: 0,
                    index: activeIndex
                }
            ]);

            chart.update();
        });
    }
};

Регистрация:

Chart.register(keyboardNavigationPlugin);

Преимущества plugin-подхода

Плагин позволяет:

  • переиспользовать логику;
  • стандартизировать accessibility;
  • централизовать обработчики;
  • подключать поддержку клавиатуры автоматически.

Частые ошибки

Отсутствие tabindex

Без tabindex canvas недоступен для клавиатуры.


Навигация без визуального выделения

Пользователь не понимает текущий элемент.


Отсутствие ARIA

Screen reader не получает информацию.


Использование только tooltip

Canvas tooltip часто недоступен для assistive technologies.


Перехват всех клавиш

Необходимо избегать конфликтов с браузером и screen reader.

Плохой пример:

event.preventDefault();

для всех клавиш подряд.


Рекомендации по accessibility

Использование контрастных цветов

Активный элемент должен быть хорошо различим.


Поддержка screen reader

Минимальный набор:

  • aria-label;
  • aria-describedby;
  • aria-live.

Предоставление таблицы данных

Табличное представление остаётся наиболее доступным форматом.


Поддержка логичной навигации

Клавиши должны работать предсказуемо:

  • ← →
  • ↑ ↓
  • Home
  • End
  • Enter
  • Escape

Интеграция с WCAG

Поддержка клавиатуры напрямую связана с требованиями WCAG.

Особенно важны:

  • WCAG 2.1.1 Keyboard;
  • WCAG 2.4.3 Focus Order;
  • WCAG 4.1.2 Name, Role, Value.

Архитектура полноценной keyboard navigation системы

Крупные приложения обычно строят систему из нескольких компонентов:

Отвечает за:

  • обработку клавиш;
  • индексы;
  • состояние фокуса.

Accessibility Layer

Содержит:

  • ARIA;
  • live regions;
  • screen reader тексты.

Rendering Layer

Управляет:

  • выделением;
  • tooltip;
  • анимацией.

Synchronization Layer

Связывает:

  • таблицы;
  • фильтры;
  • dashboard widgets;
  • внешние элементы интерфейса.