Plus · DragMove

DragMove

Set elements to change their position by dragging

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
Drag me
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

Constrainer
Drag me
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.

Constrainer
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.

蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制

Constrainer
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

PropertyDescriptionTypeDefault
allowInputDragWhether dragging is allowed when pressing on a native input or textarea.booleanfalse
allowMoveFunction that decides whether a drag may start on pointer down; returning false prevents it.Type(e: PointerEvent, element: HTMLElement) => boolean-
childrenThe content to drag. Each top-level DOM element is dragged on its own; text nodes cannot be dragged.JSX.Element-
constrainerThe 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)-
customMoveCustom 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-
handlerReturns the element that triggers dragging. Falls back to the dragged element itself when it returns null.Type() => HTMLElement | null-
onMouseDownCalled when a mouse or pen pointer is pressed.Type(e: PointerEvent) => void-
onMouseMoveCalled when a mouse or pen pointer moves.Type(e: PointerEvent) => void-
onMouseUpCalled when a mouse or pen pointer is released.Type(e: PointerEvent) => void-
onTouchCancelCalled when the pointer is cancelled by the system (pointercancel).Type(e: PointerEvent) => void-
onTouchEndCalled when a touch pointer is released.Type(e: PointerEvent) => void-
onTouchMoveCalled when a touch pointer moves.Type(e: PointerEvent) => void-
onTouchStartCalled when a touch pointer is pressed.Type(e: PointerEvent) => void-
positionStrategyPositioning 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.