When to use
It is used to set the element that can be dragged to change its position. It supports limiting the drag range and customizing the elements that trigger dragging.
Demos
How to introduce
import { DragMove } from '@fibrous/semi-ui-solid';
Basic usage
Elements wrapped by DragMove will be able to change their position by dragging.
Notice
- DragMove uses absolute positioning by default. Set
positionStrategy="relative"when the element should keep its original layout position, such as a centered Modal
- DragMove binds its listeners to each top-level DOM element rendered by
children, and each top-level element is dragged on its own. Text nodes cannot be dragged, so wrap them in an element; top-level elements rendered by a custom component are bound too, and it does not need to pass props through or forward a ref
Code
import { DragMove } from '@fibrous/semi-ui-solid';
export default function DragMoveDemo() {
// DragMove positions the element absolutely, so the wrapper keeps room for it in the preview
return (
<div style={{ height: '120px' }}>
<DragMove>
<div
style={{
'background-color': 'var(--semi-color-primary)',
width: '80px',
height: '80px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'border-radius': '10px',
'font-weight': '500',
position: 'absolute',
color: 'var(--semi-white)',
}}
>
Drag me
</div>
</DragMove>
</div>
);
}Limit drag range
Pass in constrainer, either 'parent' or a function that returns the element limiting the draggable range.
Note: The elements returned by the constrainer need to be positioned relative
Code
import { DragMove } from '@fibrous/semi-ui-solid';
export default function DragMoveDemo() {
let container: HTMLDivElement | undefined;
return (
<div
ref={(element) => (container = element)}
style={{
'background-color': 'var(--semi-grey-2)',
width: '300px',
height: '300px',
padding: '5px',
position: 'relative',
color: 'var(--semi-white)',
'font-weight': '500',
}}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove constrainer={() => container ?? null}>
<div
style={{
'background-color': 'var(--semi-color-primary)',
width: '80px',
height: '80px',
'border-radius': '10px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
position: 'absolute',
top: '80px',
left: '80px',
}}
>
Drag me
</div>
</DragMove>
</div>
);
}Customize elements that trigger dragging
Passing in handler, this function returns the element that triggered the drag. If not set, you can click anywhere to drag; if set, only the part of the element returned by the handler can be dragged.
Code
import { IconTransparentStroked } from '@fibrous/semi-icons-solid';
import { DragMove } from '@fibrous/semi-ui-solid';
export default function DragMoveDemo() {
let handlerElement: HTMLDivElement | undefined;
let container: HTMLDivElement | undefined;
return (
<div
ref={(element) => (container = element)}
style={{
'background-color': 'var(--semi-grey-2)',
width: '300px',
height: '300px',
padding: '5px',
position: 'relative',
color: 'var(--semi-white)',
'font-weight': '500',
}}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove handler={() => handlerElement ?? null} constrainer={() => container ?? null}>
<div
style={{
'background-color': 'var(--semi-color-primary)',
width: '80px',
height: '80px',
'border-radius': '10px',
position: 'absolute',
top: '50px',
left: '50px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
}}
>
<div ref={(element) => (handlerElement = element)} style={{ width: 'fit-content', height: 'fit-content' }}>
<IconTransparentStroked size="large" />
</div>
</div>
</DragMove>
</div>
);
}Customize position processing after dragging
You can customize the position processing after dragging through customMove. After this parameter is set, the DragMove component will only return the calculated position through the parameters without setting it. The user can set the new position as needed.
Code
import { DragMove } from '@fibrous/semi-ui-solid';
export default function DragMoveDemo() {
let containerRef: HTMLDivElement | undefined;
let elementRef: HTMLButtonElement | undefined;
let startPoint: { x: number; y: number } | null = null;
const customMove = (element: HTMLElement, top: number, left: number) => {
const container = containerRef;
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 = (e: MouseEvent) => {
startPoint = {
x: e.clientX,
y: e.clientY,
};
};
const toggleWidth = () => {
if (!elementRef) return;
elementRef.style.width = elementRef.style.width === '60px' ? '100px' : '60px';
};
const onMouseUp = (e: MouseEvent) => {
if (startPoint) {
const { x, y } = startPoint;
if (Math.abs(e.clientX - x) < 5 && Math.abs(e.clientY - y) < 5) {
toggleWidth();
}
}
startPoint = null;
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
toggleWidth();
}
};
return (
<>
<span>蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制 </span>
<br />
<br />
<div
ref={(element) => (containerRef = element)}
style={{
'background-color': 'var(--semi-grey-2)',
width: '300px',
height: '300px',
position: 'relative',
padding: '10px',
color: 'var(--semi-white)',
'font-weight': '500',
}}
>
<span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
<DragMove constrainer={() => containerRef ?? null} customMove={customMove}>
<button
type="button"
style={{
'background-color': 'var(--semi-color-primary)',
'box-sizing': 'content-box',
width: '60px',
height: '50px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
position: 'absolute',
top: '50px',
left: '50px',
border: 'none',
'border-radius': '10px',
padding: '5px',
color: 'inherit',
font: 'inherit',
}}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onKeyDown={onKeyDown}
ref={(element) => (elementRef = element)}
>
Drag me
</button>
</DragMove>
</div>
</>
);
}API
Props
| Property | Description | Type | Default |
|---|---|---|---|
| allowInputDrag | Whether dragging is allowed when pressing on a native input or textarea. | boolean | false |
| allowMove | Function that decides whether a drag may start on pointer down; returning false prevents it. | Type(e: PointerEvent, element: HTMLElement) => boolean | - |
| children | The content to drag. Each top-level DOM element is dragged on its own; text nodes cannot be dragged. | JSX.Element | - |
| constrainer | The element that limits the draggable range: 'parent' for the parent element, or a function that returns it. It needs to be positioned relatively. | Type'parent' | (() => HTMLElement | null) | - |
| customMove | Custom position handling after dragging. When set, the component only passes the calculated position back in requestAnimationFrame and no longer writes the element's top and left. | Type(element: HTMLElement, top: number, left: number) => void | - |
| handler | Returns the element that triggers dragging. Falls back to the dragged element itself when it returns null. | Type() => HTMLElement | null | - |
| onMouseDown | Called when a mouse or pen pointer is pressed. | Type(e: PointerEvent) => void | - |
| onMouseMove | Called when a mouse or pen pointer moves. | Type(e: PointerEvent) => void | - |
| onMouseUp | Called when a mouse or pen pointer is released. | Type(e: PointerEvent) => void | - |
| onTouchCancel | Called when the pointer is cancelled by the system (pointercancel). | Type(e: PointerEvent) => void | - |
| onTouchEnd | Called when a touch pointer is released. | Type(e: PointerEvent) => void | - |
| onTouchMove | Called when a touch pointer moves. | Type(e: PointerEvent) => void | - |
| onTouchStart | Called when a touch pointer is pressed. | Type(e: PointerEvent) => void | - |
| positionStrategy | Positioning strategy. relative keeps the element's original layout position. | 'absolute' | 'relative' | "absolute" |
DragMove has no root element of its own, so undeclared attributes such as class, style and ref have nowhere to land and are dropped. Put what you need directly on the dragged element.