Plus 组件

拖拽移动

可通过拖拽改变位置

使用场景

用于设置元素可被拖动改变位置,支持限制拖拽范围,支持自定义触发拖动的元素。

代码演示

如何引入

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 上按下时是否允许拖动。booleanfalse
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 没有自己的根元素,未声明的属性会被丢弃。