为现有浮动元素(例如面板、调色板、工具栏、检查器或对话框)添加拖动功能。拖动操作不会决定元素的显示、关闭、堆叠或聚焦方式。
使用专用标识名
当元素包含可选择的文本、按钮、输入内容或链接时,请从专用手柄开始拖动。这样可以确保其正常内容可用。使滑块可聚焦,以便通过键盘移动,并为其指定一个提及箭头键互动的无障碍名称。
请勿对标识名应用 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 初始化时附加其拖动功能。
在 user-select: none 之前添加 -webkit-user-select: none,以便在 Safari 中拖动时保持文本选择处于禁止状态。