ally.js — это библиотека JavaScript, предназначенная для обеспечения доступности веб-приложений. Одной из ключевых проблем при реализации drag-and-drop (D&D) является обеспечение управления с клавиатуры, чтобы пользователи, которые не используют мышь, могли полноценно взаимодействовать с интерфейсом.
В D&D с клавиатуры важно разделять три состояния:
ally.js предоставляет функции для управления фокусом и проверкой доступности элементов, что упрощает реализацию этих состояний.
Базовая функция для установки фокуса:
import * as ally from 'ally.js';
const element = document.querySelector('#draggable');
ally.focus.element(element);
Ключевые моменты:
ally.focus.element() гарантирует, что элемент станет
видимым для экранного читателя.ally.query.tabsequence() для
получения списка всех элементов, доступных через Tab. Это позволяет
динамически определить, куда элемент может быть перемещён при
клавиатурной навигации.const tabbable = ally.query.tabsequence({ context: document });
tabbable возвращает массив объектов с информацией о
каждом элементе, включая координаты, видимость и доступность.Для активации D&D с клавиатуры необходимо добавить обработчик событий клавиатуры. Обычно используется клавиша Enter или Space для «поднятия» элемента:
element.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
element.setAttribute('aria-grabbed', 'true');
element.classList.add('dragging');
event.preventDefault();
}
});
Важно:
aria-grabbed сигнализирует о том, что элемент
выбран для перемещения.dragging позволяет визуально выделить
элемент.event.preventDefault() предотвращает стандартное
поведение клавиш, чтобы не нарушить логику D&D.После поднятия элемента пользователь должен иметь возможность перемещать его к другим позициям с клавиатуры. ally.js помогает определить элементы, куда можно «перетащить» объект.
const dropTargets = ally.query.tabsequence({ context: container, includeOnlyTabbable: true });
Для перемещения можно использовать стрелочные клавиши:
document.addEventListener('keydown', (event) => {
if (element.classList.contains('dragging')) {
let currentIndex = dropTargets.indexOf(document.activeElement);
if (event.key === 'ArrowDown') {
let nextIndex = (currentIndex + 1) % dropTargets.length;
ally.focus.element(dropTargets[nextIndex].node);
event.preventDefault();
}
if (event.key === 'ArrowUp') {
let prevIndex = (currentIndex - 1 + dropTargets.length) % dropTargets.length;
ally.focus.element(dropTargets[prevIndex].node);
event.preventDefault();
}
}
});
Особенности реализации:
dropTargets[index].node — реальный DOM-элемент для
фокуса.Для сброса элемента на выбранную позицию применяются те же клавиши, что и для выбора (обычно Enter или Space):
document.addEventListener('keydown', (event) => {
if (element.classList.contains('dragging') && (event.key === 'Enter' || event.key === ' ')) {
element.setAttribute('aria-grabbed', 'false');
element.classList.remove('dragging');
event.preventDefault();
}
});
Дополнительно можно обновлять DOM, перемещая элемент в новую позицию относительно текущего фокуса.
Для полноценной доступности следует использовать:
aria-grabbed="true|false" — состояние выбранного
элемента.aria-dropeffect="move" — тип операции на цели.role="list" и role="listitem" для
контейнеров и элементов..dragging {
outline: 2px dashed #007acc;
background-color: #e6f7ff;
}
Библиотека позволяет убедиться, что после перемещения элемент остаётся доступным:
ally.query.firstTabbable({ context: container });
ally.query.focused({ context: document });
Эти функции помогают:
ally.js можно комбинировать с Dragula, SortableJS или нативными drag-and-drop API:
idle,
dragging, dropTargetFocused.