使用场景
用于设置元素可被拖动改变位置,支持限制拖拽范围,支持自定义触发拖动的元素。
代码演示
如何引入
import { DragMove } from '@fibrous/semi-ui-react';
基本用法
被 DragMove 包裹的元素将能够通过拖拽改变位置。
注意
- DragMove 默认会将可拖拽的元素设置为 absolute 定位;需要保留元素原有布局位置(例如居中的 Modal)时,可设置
positionStrategy="relative"
- DragMove 把监听绑定在内容渲染出的每个顶层真实 DOM 节点上,每个顶层节点各自可拖动。内容可以是 DOM 元素,也可以是自定义组件,自定义组件无需透传 props 或转发 ref
Drag me
代码
import { DragMove } from '@fibrous/semi-ui-react';
export default function DragMoveDemo() {
// DragMove positions the element absolutely, so the wrapper keeps room for it in the preview
return (
<div style={{ height: 120 }}>
<DragMove>
<div
style={{
backgroundColor: 'var(--semi-color-primary)',
width: 80,
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 10,
fontWeight: 500,
position: 'absolute',
color: 'var(--semi-white)',
}}
>
Drag me
</div>
</DragMove>
</div>
);
}限制拖动范围
传入 constrainer,可以是 'parent',也可以是返回限制可拖拽范围的元素的函数。
注意:constrainer 设置的元素需要为 relative 定位
Constrainer
Drag me
代码
import React from 'react';
import { DragMove } from '@fibrous/semi-ui-react';
export default function DragMoveDemo() {
const containerRef = React.useRef<HTMLDivElement>(null);
return (
<div
style={{
backgroundColor: 'var(--semi-grey-2)',
width: 300,
height: 300,
padding: 5,
position: 'relative',
color: 'var(--semi-white)',
fontWeight: 500,
}}
ref={containerRef}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove constrainer={() => containerRef.current}>
<div
style={{
backgroundColor: 'var(--semi-color-primary)',
width: 80,
height: 80,
borderRadius: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 80,
left: 80,
}}
>
Drag me
</div>
</DragMove>
</div>
);
}自定义触发拖动的元素
可通过 handler 自定义触发拖动的元素。如果不设置, 则点击任意位置均可拖动;如果设置,则仅点击 handler 部分可拖动。
Constrainer
代码
import React from 'react';
import { IconTransparentStroked } from '@fibrous/semi-icons-react';
import { DragMove } from '@fibrous/semi-ui-react';
export default function DragMoveDemo() {
const handlerRef = React.useRef<HTMLDivElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
return (
<div
style={{
backgroundColor: 'var(--semi-grey-2)',
width: 300,
height: 300,
padding: 5,
position: 'relative',
color: 'var(--semi-white)',
fontWeight: 500,
}}
ref={containerRef}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove handler={() => handlerRef.current} constrainer={() => containerRef.current}>
<div
style={{
backgroundColor: 'var(--semi-color-primary)',
width: 80,
height: 80,
borderRadius: 10,
position: 'absolute',
top: 50,
left: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ width: 'fit-content', height: 'fit-content' }} ref={handlerRef}>
<IconTransparentStroked size="large" />
</div>
</div>
</DragMove>
</div>
);
}自定义拖动后的位置处理
可通过 customMove 自定义拖动后的位置处理,该参数设置后,DragMove 组件内部将仅通过参数返回计算后的位置,不做设置,用户按需自行设置新位置。
Constrainer
代码
import React, { useCallback, type KeyboardEvent, type MouseEvent } from 'react';
import { DragMove } from '@fibrous/semi-ui-react';
export default function DragMoveDemo() {
const containerRef = React.useRef<HTMLDivElement>(null);
const elementRef = React.useRef<HTMLButtonElement>(null);
const startPoint = React.useRef<{ x: number; y: number } | null>(null);
const customMove = useCallback((element: HTMLElement, top: number, left: number) => {
const container = containerRef.current;
if (container && left + 100 > container.offsetWidth) {
element.style.right = `${container.offsetWidth - left - element.offsetWidth}px`;
element.style.left = 'auto';
} else {
element.style.left = left + 'px';
}
element.style.top = top + 'px';
}, []);
const onMouseDown = useCallback((e: MouseEvent) => {
startPoint.current = {
x: e.clientX,
y: e.clientY,
};
}, []);
const toggleWidth = useCallback(() => {
const element = elementRef.current;
if (!element) return;
element.style.width = element.style.width === '60px' ? '100px' : '60px';
}, []);
const onMouseUp = useCallback(
(e: MouseEvent) => {
if (startPoint.current) {
const { x, y } = startPoint.current;
if (Math.abs(e.clientX - x) < 5 && Math.abs(e.clientY - y) < 5) {
toggleWidth();
}
}
startPoint.current = null;
},
[toggleWidth]
);
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter') {
toggleWidth();
}
},
[toggleWidth]
);
return (
<>
<span>蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制 </span>
<br />
<br />
<div
style={{
backgroundColor: 'var(--semi-grey-2)',
width: 300,
height: 300,
position: 'relative',
padding: 10,
color: 'var(--semi-white)',
fontWeight: 500,
}}
ref={containerRef}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove constrainer={() => containerRef.current} customMove={customMove}>
<button
type="button"
style={{
backgroundColor: 'var(--semi-color-primary)',
boxSizing: 'content-box',
width: 60,
height: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 50,
left: 50,
border: 'none',
borderRadius: 10,
padding: 5,
color: 'inherit',
font: 'inherit',
}}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onKeyDown={onKeyDown}
ref={elementRef}
>
Drag me
</button>
</DragMove>
</div>
</>
);
}API
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| allowInputDrag | 在原生 input、textarea 上按下时是否允许拖动。 | boolean | false |
| allowMove | 按下时判断是否允许拖动的函数,返回 false 则不拖动。参数为指针事件与被拖动的元素。 | Type(e: PointerEvent, element: HTMLElement) => boolean | - |
| children | 被拖动的内容。每个顶层 DOM 元素各自可拖动,自定义组件渲染出的顶层元素同样生效;文本节点无法拖动,需要包在元素中。 | ReactNode | - |
| constrainer | 限制拖动范围的元素:'parent' 表示被拖动元素的父元素,或返回该元素的函数;返回 null 时不限制范围。该元素需要为 relative 定位。 | Type'parent' | (() => HTMLElement | null) | - |
| customMove | 自定义拖动后的位置处理,参数为元素、新的 top 与 left。设置后组件不再改写元素的 top 与 left,位置在 requestAnimationFrame 中回传。 | Type(e: HTMLElement, top: number, left: number) => void | - |
| handler | 返回触发拖动的元素,只有在该元素内按下才会开始拖动;返回 null 时回退为被拖动元素本身。 | Type() => HTMLElement | null | - |
| onMouseDown | 鼠标或触控笔按下时触发,鼠标只响应主键;不受 allowMove 的结果影响。 | Type(e: PointerEvent) => void | - |
| onMouseMove | 拖动过程中鼠标或触控笔移动时触发。 | Type(e: PointerEvent) => void | - |
| onMouseUp | 拖动开始后,鼠标或触控笔抬起时触发。 | Type(e: PointerEvent) => void | - |
| onTouchCancel | 拖动过程中指针被系统取消(pointercancel)时触发。 | Type(e: PointerEvent) => void | - |
| onTouchEnd | 拖动开始后,触摸抬起时触发。 | Type(e: PointerEvent) => void | - |
| onTouchMove | 拖动过程中触摸移动时触发。 | Type(e: PointerEvent) => void | - |
| onTouchStart | 触摸按下时触发,不受 allowMove 的结果影响。 | Type(e: PointerEvent) => void | - |
| positionStrategy | 定位策略,relative 保留元素原有的布局位置。 | 'absolute' | 'relative' | "absolute" |
DragMove 没有自己的根元素,未声明的属性会被丢弃。