Добавьте возможность перетаскивания для существующего плавающего элемента, например панели, палитры, панели инструментов, инспектора или диалогового окна. Перетаскивание не определяет, как элемент отображается, закрывается, укладывается или фокусируется.
Используйте специальный псевдоним
Если элемент содержит выделяемый текст, кнопки, поля ввода или ссылки, перетаскивайте его за специальный маркер. Это позволяет использовать его обычное содержимое. Сделайте так, чтобы на ползунок можно было перевести фокус с помощью клавиатуры, и добавьте доступное название, в котором упоминается взаимодействие с клавишами со стрелками.
Не наносите role="application" на ручку. Это неоправданно меняет взаимодействие со вспомогательными технологиями.
Примените cursor: grab/grabbing, touch-action: none, user-select: none и -webkit-user-select: none к маркеру, а не ко всему перетаскиваемому элементу. Тогда текст за его пределами можно будет выделять, а жесты будут работать как обычно.
.drag-handle {
cursor: grab;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
.drag-handle.dragging {
cursor: grabbing;
}
Сохранять положение указателя и доступность
При ограничении движения областью просмотра используйте модель позиционирования, связанную с областью просмотра, например position: fixed.
- Начать перетаскивание только для основной кнопки указателя (
event.button === 0). - Запишите смещение указателя на
pointerdownи используйте его при каждом перемещении, чтобы элемент не прыгал, когда его захватывают за угол. - Продолжать перетаскивание, когда указатель покидает маркер. По умолчанию используется
setPointerCapture(), а приlostpointercaptureсостояние перетаскивания сбрасывается. Если маркер может быть удален или повторно отрисован в процессе перетаскивания, используйте прослушивателиpointermove,pointerupиpointercancelна уровне документа, поскольку в этом случае захват теряется. - При желании можно настроить задержку, чтобы перетаскивание начиналось только после перемещения на 4 пикселя. Это позволит избежать изменения стиля при простом нажатии на маркер.
При захвате указателя прослушиватели движения могут оставаться на маркере:
handle.addEventListener('pointerdown', (event) => {
if (event.button !== 0 || event.target.closest('button, input, select, textarea, a')) return;
const rect = draggable.getBoundingClientRect();
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
handle.setPointerCapture(event.pointerId);
});
handle.addEventListener('pointermove', (event) => {
if (!handle.hasPointerCapture(event.pointerId)) return;
handle.classList.add('dragging');
moveTo(event.clientX - offsetX, event.clientY - offsetY);
});
handle.addEventListener('lostpointercapture', () => {
handle.classList.remove('dragging');
});
function moveTo(left, top) {
const maxLeft = Math.max(0, window.innerWidth - draggable.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - draggable.offsetHeight);
draggable.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`;
draggable.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
}
Ограничьте каждую позицию указателя и клавиатуры с помощью одного и того же помощника, чтобы элемент оставался доступным. Повторно закрепить текущую позицию после изменения размера области просмотра или элемента. Для элементов с большим количеством макетов рассмотрите возможность перевода во время перетаскивания, чтобы избежать макета для каждого кадра, а затем зафиксируйте окончательную позицию в left и top при выпуске.
Перемещение с помощью клавиатуры
Когда маркер находится в фокусе, переместите элемент с помощью клавиш со стрелками и вызовите preventDefault() для этих клавиш. Используйте тот же помощник по позиционированию, что и для перемещения указателя. Шаг в 15 пикселей – разумная отправная точка. Адаптируйте его к размеру компонента и требованиям к точности.
Примечание для диалогового окна
<dialog> или [popover] могут использовать этот механизм с заголовком в качестве дескриптора. Задайте margin: 0 перед назначением left и top, поскольку поля браузера по умолчанию мешают позиционированию вручную. Подробнее о том, как открывать, закрывать и отклонять диалоговые и всплывающие окна, а также управлять фокусом, рассказано в статьях Декларативное управление диалоговыми и всплывающими окнами и Элементы управления платформы закрывают диалоговое окно.
Резервные стратегии
Если перетаскивание только меняет положение компонента, рассматривайте его как прогрессивное улучшение: без JavaScript элемент, его контент и элементы управления должны быть видимыми, читаемыми и функциональными в разумном положении по умолчанию. Не показывайте неработающий маркер перетаскивания. Если маркер является отдельным элементом управления, скройте его по умолчанию и покажите при инициализации JavaScript. Если маркер совмещен с заголовком, добавьте его возможности перетаскивания при инициализации JavaScript.
Добавьте -webkit-user-select: none перед user-select: none, чтобы текст не выделялся при перетаскивании в Safari.