react-dnd 踩坑-原生拖拽事件失效
近期工作中使用到了 react-dnd
开发拖拽功能,开发过程中发现页面中原来使用的一个第三方组件的拖拽功能不能正常工作了。
分析
将组件单独引入页面发现工作正常,排除了第三方组件版本的原因。后面发现是 react-dnd
引入后出现的问题,猜测是 react-dnd
引入影响了原有拖拽。
解决方案
在 github 上查找到相关的 issues,有不少人遇到了同样的问题,最终在 issues/3344 中找到了答案。
具体demo可以查看 codesandbox
解决方案主要是在react-dnd
初始化时传入了rootElement
。
import React, { useState, useCallback, useMemo } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
export const useDndProvider = () => {
const [dndArea, setDndArea] = useState();
const handleRef = useCallback((node) => setDndArea(node), []);
const html5Options = useMemo(
() => ({ rootElement: dndArea }),
[dndArea]
);
return { dndArea, handleRef, html5Options };
};
const DndProviderWrapper = (props) => {
const { dndArea, handleRef, html5Options } = useDndProvider();
return (
<div ref={handleRef}>
{dndArea && (
<DndProvider backend={HTML5Backend} options={html5Options}>
{props.children}
</DndProvider>
)}
</div>
);
};
export default DndProviderWrapper;
查看官方文档发现没有关于 rootElement
的描述,看看 react-dnd-html5-backend
源码是否能找出 rootElement
的作用。
import type { BackendFactory, DragDropManager } from 'dnd-core'
import { HTML5BackendImpl } from './HTML5BackendImpl.js'
import type { HTML5BackendContext, HTML5BackendOptions } from './types.js'
export { getEmptyImage } from './getEmptyImage.js'
export * as NativeTypes from './NativeTypes.js'
export type { HTML5BackendContext, HTML5BackendOptions } from './types.js'
export const HTML5Backend: BackendFactory = function createBackend(
manager: DragDropManager,
context?: HTML5BackendContext,
options?: HTML5BackendOptions,
): HTML5BackendImpl {
return new HTML5BackendImpl(manager, context, options)
}
我们看看 HTML5BackendOptions
的定义
export type HTML5BackendContext = Window | undefined
/**
* Configuration options for the HTML5Backend
*/
export interface HTML5BackendOptions {
/**
* The root DOM node to use for subscribing to events. Default=Window
*/
rootElement: Node
}
其实这里我们已经可以看到 rootElement
的作用了,是用于订阅事件的根 DOM 节点,默认是 Window
对象。再看看具体是怎么使用的。
export class HTML5BackendImpl implements Backend {
public constructor(
manager: DragDropManager,
globalContext?: HTML5BackendContext,
options?: HTML5BackendOptions,
) {
this.options = new OptionsReader(globalContext, options)
this.actions = manager.getActions()
this.monitor = manager.getMonitor()
this.registry = manager.getRegistry()
this.enterLeaveCounter = new EnterLeaveCounter(this.isNodeInDocument)
}
//...
public setup(): void {
const root = this.rootElement as RootNode | undefined
if (root === undefined) {
return
}
if (root.__isReactDndBackendSetUp) {
throw new Error('Cannot have two HTML5 backends at the same time.')
}
root.__isReactDndBackendSetUp = true
this.addEventListeners(root)
}
private addEventListeners(target: Node) {
// SSR Fix (https://github.com/react-dnd/react-dnd/pull/813
if (!target.addEventListener) {
return
}
target.addEventListener(
'dragstart',
this.handleTopDragStart as EventListener,
)
target.addEventListener('dragstart', this.handleTopDragStartCapture, true)
target.addEventListener('dragend', this.handleTopDragEndCapture, true)
target.addEventListener(
'dragenter',
this.handleTopDragEnter as EventListener,
)
target.addEventListener(
'dragenter',
this.handleTopDragEnterCapture as EventListener,
true,
)
target.addEventListener(
'dragleave',
this.handleTopDragLeaveCapture as EventListener,
true,
)
target.addEventListener('dragover', this.handleTopDragOver as EventListener)
target.addEventListener(
'dragover',
this.handleTopDragOverCapture as EventListener,
true,
)
target.addEventListener('drop', this.handleTopDrop as EventListener)
target.addEventListener(
'drop',
this.handleTopDropCapture as EventListener,
true,
)
}
}
总结
rootElement
作为根节点元素监听事件,没有传值则默认是Window
对象。这个属性的说明还是比较重要的,但官方文档没有说明,还是忍不住吐槽一下-。-