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-react';

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 real DOM node that the content renders, and each top-level node is dragged on its own. The content can be DOM elements or custom components; a custom component does not need to pass props through or forward a ref
Drag me
Code
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>
    );
}

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 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>
    );
}

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 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>
    );
}

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

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. It receives the pointer event and the dragged element.Type(e: PointerEvent, element: HTMLElement) => boolean-
childrenThe content to drag. Each top-level DOM element is dragged on its own, including the top-level elements of a custom component. Text nodes cannot be dragged and need to be wrapped in an element.ReactNode-
constrainerThe element that limits the draggable range: 'parent' for the parent of the dragged element, or a function that returns it. When it returns null, the range is not limited. It needs to be positioned relatively.Type'parent' | (() => HTMLElement | null)-
customMoveCustom position handling that receives the element, the new top and the new left. When set, the component no longer writes the element's top and left, and passes the position back in requestAnimationFrame.Type(e: HTMLElement, top: number, left: number) => void-
handlerReturns the element that triggers dragging; a drag starts only when pressed inside it. Falls back to the dragged element itself when it returns null.Type() => HTMLElement | null-
onMouseDownCalled when a mouse or pen pointer is pressed, and only for the primary button of a mouse. Not affected by the result of allowMove.Type(e: PointerEvent) => void-
onMouseMoveCalled when a mouse or pen pointer moves during a drag.Type(e: PointerEvent) => void-
onMouseUpCalled when a mouse or pen pointer is released after a drag has started.Type(e: PointerEvent) => void-
onTouchCancelCalled when the pointer is cancelled by the system (pointercancel) during a drag.Type(e: PointerEvent) => void-
onTouchEndCalled when a touch pointer is released after a drag has started.Type(e: PointerEvent) => void-
onTouchMoveCalled when a touch pointer moves during a drag.Type(e: PointerEvent) => void-
onTouchStartCalled when a touch pointer is pressed, whether or not allowMove allows the drag.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 are dropped.