diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md new file mode 100644 index 0000000000..0552c8701d --- /dev/null +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md @@ -0,0 +1,58 @@ +--- +path: '/docs/api/use-drag' +title: 'useDrag' +--- + +## EXPERIMENTAL API - UNSTABLE + +_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ + +# useDrag + +A hook to use the current component as a drag-source. + +```js +import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +const { + useDrag, +} = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ + +function myDraggable(props) { + const collectedProps = useDrag(spec) +} +``` + +### useDrag Parameters + +- **`spec`** Specification object, see below for details on how to construct this + +### Return Value + +useDrag returns an array: + +0. An object containing collected properties from the collect function. If no `collect` function is defined, an empty object is returned. +1. The React ref to use. This is automatically created if no `ref` field is defined on the specification object. + +### Specification Object Members + +- **`ref`**: Required. A ref object to use to attach to the draggable element. + +- **`preview`**: Optional. An HTML Element or a ref object attached to the dragPreview element. + +- **`previewOptions`**: Optional. A plain JavaScript object describing drag preview options. + +- **`item`**: Required. A plain JavaScript object describing the data being dragged. This is the _only_ information available to the drop targets about the drag source so it's important to pick the _minimal_ data they need to know. You may be tempted to put a complex reference here, but you should try very hard to avoid doing this because it couples the drag sources and drop targets. It's a good idea to return something like `{ type, id }` from this method. + +- **`item.type`**: Required. Either a string, an ES6 symbol`. Only the [drop targets](/docs/api/drop-target) registered for the same type will react to the items produced by this drag source. Read the [overview](/docs/overview) to learn more about the items and types. + +- **`options`**: Optional. A plain object. If some of the props to your component are not scalar (that is, are not primitive values or functions), specifying a custom`arePropsEqual(props, otherProps)`function inside the`options` object can improve the performance. Unless you have performance problems, don't worry about it. + +- **`begin(monitor)`**: Optionaln. Fired when a drag operation begins. + +- **`end(monitor)`**: Optional. When the dragging stops, `end` is called. For every `begin` call, a corresponding `end` call is guaranteed. You may call `monitor.didDrop()` to check whether or not the drop was handled by a compatible drop target. If it was handled, and the drop target specified a _drop result_ by returning a plain object from its `drop()` method, it will be available as `monitor.getDropResult()`. This method is a good place to fire a Flux action. _Note: If the component is unmounted while dragging, `component` parameter is set to be `null`._ + +- **`canDrag(monitor)`**: Optional. Use it to specify whether the dragging is currently allowed. If you want to always allow it, just omit this method. Specifying it is handy if you'd like to disable dragging based on some predicate over `props`. _Note: You may not call `monitor.canDrag()` inside this method._ + +- **`isDragging(monitor)`**: Optional. By default, only the drag source that initiated the drag operation is considered to be dragging. You can override this behavior by defining a custom `isDragging` method. It might return something like `props.id === monitor.getItem().id`. Do this if the original component may be unmounted during the dragging and later “resurrected” with a different parent. For example, when moving a card across the lists in a Kanban board, you want it to retain the dragged appearance—even though technically, the component gets unmounted and a different one gets mounted every time you move it to another list. _Note: You may not call `monitor.isDragging()` inside this method._ + +* **`collect`**: Optional. The collecting function. It should return a plain object of the props to return for injection into your component. It receives two parameters, `monitor` and `props`. Read the [overview](/docs/overview) for an introduction to the monitors and the collecting function. See the collecting function described in detail in the next section. diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDragLayer.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDragLayer.md new file mode 100644 index 0000000000..708a227bb3 --- /dev/null +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDragLayer.md @@ -0,0 +1,32 @@ +--- +path: '/docs/api/use-drag-layer' +title: 'useDrag' +--- + +## EXPERIMENTAL API - UNSTABLE + +_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ + +# useDragLayer + +A hook to use the current component as a drag-layer. + +```js +import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +const { + useDragLayer, +} = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ + +function myDragLayer(props) { + const collectedProps = useDragLayer(spec) + ... +} +``` + +### useDragLayer Parameters + +- **`collect`**: Required. The collecting function. It should return a plain object of the props to return for injection into your component. It receives two parameters, `monitor` and `props`. Read the [overview](/docs/overview) for an introduction to the monitors and the collecting function. See the collecting function described in detail in the next section. + +### Return Value + +useDragLayer returns an object of collected properties from the collect function. diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md new file mode 100644 index 0000000000..61c9cd5334 --- /dev/null +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md @@ -0,0 +1,45 @@ +--- +path: '/docs/api/use-drag-preview' +title: 'useDragPreview' +--- + +## EXPERIMENTAL API - UNSTABLE + +_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ + +# useDragPreview + +A hook to use the current component as a drag-layer. + +```js +import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +const { + useDragPreview, +} = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ + +function myDragLayer(props) { + const [preview, DragPreview] = useDragPreview(spec) + const collectedProps = useDrag({ + ... + preview + }) + + return ( + <> + + <... rest of item... /> + + ) +} +``` + +### useDragPreview Parameters + +- **`dragPreview`** A refForwarding component that will render the drag preview. + +### Return Value + +useDragPreview returns an array of two items: + +0. The drag preview ref object. This should be passed into useDrag's specification +1. A component to render the dragPreview in your render method. diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md new file mode 100644 index 0000000000..724332c585 --- /dev/null +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md @@ -0,0 +1,50 @@ +--- +path: '/docs/api/use-drop' +title: 'useDrop' +--- + +## EXPERIMENTAL API - UNSTABLE + +_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ + +# useDrop + +A hook to use the current component as a drop target. + +```js +import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +const { + useDrop, +} = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ + +function myDropTarget(props) { + const collectedProps = useDrop(spec) +} +``` + +### useDrop Parameters + +- **`spec`** Specification object, see below for details on how to construct this + +### Return Value + +useDrop returns an array: + +0. An object containing collected properties from the collect function. If no `collect` function is defined, an empty object is returned. +1. The React ref to use. This is automatically created if no `ref` field is defined on the specification object. + +### Specification Object Members + +- **`ref`**: Required. A ref object to use to attach to the draggable element. + +* **`accept`**: Required. A string, an ES6 symbol, an array of either, or a function that returns either of those, given component's `props`. This drop target will only react to the items produced by the [drag sources](/docs/api/drag-source) of the specified type or types. Read the [overview](/docs/overview) to learn more about the items and types. + +- **`options`**: Optional. A plain object. If some of the props to your component are not scalar (that is, are not primitive values or functions), specifying a custom `arePropsEqual(props, otherProps)` function inside the `options` object can improve the performance. Unless you have performance problems, don't worry about it. + +- **`drop(item, monitor)`**: Optional. Called when a compatible item is dropped on the target. You may either return undefined, or a plain object. If you return an object, it is going to become _the drop result_ and will be available to the drag source in its `endDrag` method as `monitor.getDropResult()`. This is useful in case you want to perform different actions depending on which target received the drop. If you have nested drop targets, you can test whether a nested target has already handled `drop` by checking `monitor.didDrop()` and `monitor.getDropResult()`. Both this method and the source's `endDrag` method are good places to fire Flux actions. This method will not be called if `canDrop()` is defined and returns `false`. + +- **`hover(item, monitor)`**: Optional. Called when an item is hovered over the component. You can check `monitor.isOver({ shallow: true })` to test whether the hover happens over _just_ the current target, or over a nested one. Unlike `drop()`, this method will be called even if `canDrop()` is defined and returns `false`. You can check `monitor.canDrop()` to test whether this is the case. + +- **`canDrop(item, monitor)`**: Optional. Use it to specify whether the drop target is able to accept the item. If you want to always allow it, just omit this method. Specifying it is handy if you'd like to disable dropping based on some predicate over `props` or `monitor.getItem()`. _Note: You may not call `monitor.canDrop()` inside this method._ + +* **`collect`**: Optional. The collecting function. It should return a plain object of the props to return for injection into your component. It receives two parameters, `monitor` and `props`. Read the [overview](/docs/overview) for an introduction to the monitors and the collecting function. See the collecting function described in detail in the next section. diff --git a/packages/documentation/src/constants.ts b/packages/documentation/src/constants.ts index 89d2eda4ba..16235d2a2d 100644 --- a/packages/documentation/src/constants.ts +++ b/packages/documentation/src/constants.ts @@ -70,6 +70,27 @@ export const APIPages: PageGroup[] = [ }, }, }, + { + title: 'Hooks-Based API', + pages: { + USE_DRAG: { + location: '/docs/api/use-drag', + title: 'useDrag', + }, + USE_DRAG_LAYER: { + location: '/docs/api/use-drag-layer', + title: 'useDragLayer', + }, + USE_DRAG_PREVIEW: { + location: '/docs/api/use-drag-preview', + title: 'useDragPreview', + }, + USE_DROP: { + location: '/docs/api/use-drop', + title: 'useDrop', + }, + }, + }, { title: 'Connecting to DOM', pages: { diff --git a/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx b/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx index baf1c100ad..e7a9b6a061 100644 --- a/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx +++ b/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx @@ -17,10 +17,8 @@ export interface BoardSquareProps { export const BoardSquare: React.FC = ( props: BoardSquareProps, ) => { - const ref = React.useRef(null) - const { isOver, canDrop } = useDrop({ - ref, - type: ItemTypes.KNIGHT, + const [{ isOver, canDrop }, ref] = useDrop({ + accept: ItemTypes.KNIGHT, canDrop: () => canMoveKnight(props.x, props.y), drop: () => moveKnight(props.x, props.y), collect: mon => ({ diff --git a/packages/examples-hooks/src/00 Chessboard/Knight.tsx b/packages/examples-hooks/src/00 Chessboard/Knight.tsx index 8ef3fa3f03..bf3bcbdb17 100644 --- a/packages/examples-hooks/src/00 Chessboard/Knight.tsx +++ b/packages/examples-hooks/src/00 Chessboard/Knight.tsx @@ -5,6 +5,7 @@ import knightImage from './knightImage' const { useDrag, + useDragPreview, } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ const knightStyle: React.CSSProperties = { @@ -13,36 +14,38 @@ const knightStyle: React.CSSProperties = { cursor: 'move', } -function createKnightImage() { - if (typeof Image === 'undefined') { - return undefined - } - const img = new Image() - img.src = knightImage - return img -} +const KnightDragPreview = React.forwardRef( + (props, ref: React.Ref) => { + if (typeof Image === 'undefined') { + return null + } + return + }, +) export const Knight: React.FC = () => { - const ref = React.useRef(null) - const dragPreview = React.useMemo(createKnightImage, []) - const { isDragging } = useDrag({ - ref, - type: ItemTypes.KNIGHT, - preview: dragPreview, + const item = { type: ItemTypes.KNIGHT } + const [DragPreview, preview] = useDragPreview(KnightDragPreview) + const [{ isDragging }, ref] = useDrag({ + item, + preview, collect: mon => ({ isDragging: !!mon.isDragging(), }), }) return ( -
- ♘ -
+ <> + +
+ ♘ +
+ ) } diff --git a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Box.tsx index 7b6cfd1311..64db6f46f6 100644 --- a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Box.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import ItemTypes from '../Single Target/ItemTypes' + import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' const { useDrag, @@ -18,16 +19,17 @@ export interface BoxProps { name: string } -const Box: React.FC = ({ name }) => { - const ref = React.useRef(null) - const { opacity } = useDrag({ - ref, - type: ItemTypes.BOX, - begin: () => ({ name }), - end(monitor) { - const item = monitor.getItem() - const dropResult = monitor.getDropResult() +interface DropResult { + allowedDropEffect: string + dropEffect: string + name: string +} +const Box: React.FC = ({ name }) => { + const item = { name, type: ItemTypes.BOX } + const [{ opacity }, ref] = useDrag({ + item, + end(dropResult?: DropResult) { if (dropResult) { let alertMessage = '' const isDropAllowed = diff --git a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx index 176fb1127e..33af69c753 100644 --- a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx @@ -33,10 +33,8 @@ function selectBackgroundColor(isActive: boolean, canDrop: boolean) { } const Dustbin: React.FC = ({ allowedDropEffect }) => { - const ref = React.useRef(null) - const { canDrop, isOver } = useDrop({ - ref, - type: ItemTypes.BOX, + const [{ canDrop, isOver }, ref] = useDrop({ + accept: ItemTypes.BOX, drop: () => ({ name: `${allowedDropEffect} Dustbin`, allowedDropEffect, diff --git a/packages/examples-hooks/src/01 Dustbin/Copy or Move/interfaces.ts b/packages/examples-hooks/src/01 Dustbin/Copy or Move/interfaces.ts new file mode 100644 index 0000000000..21345882e2 --- /dev/null +++ b/packages/examples-hooks/src/01 Dustbin/Copy or Move/interfaces.ts @@ -0,0 +1,10 @@ +export interface DragItem { + type: string + name: string +} + +export interface DropResult { + name: string + dropEffect: string + allowedDropEffect: string +} diff --git a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx index 844b202ad7..048049d4ef 100644 --- a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx @@ -22,11 +22,9 @@ export interface BoxProps { } const Box: React.FC = ({ name, type, isDropped }) => { - const ref = React.useRef(null) - const { opacity } = useDrag({ - ref, - type, - begin: () => ({ name }), + const item = { name, type } + const [{ opacity }, ref] = useDrag({ + item, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), diff --git a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx index f22f1b5ab8..3c5fe1df06 100644 --- a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx @@ -18,21 +18,19 @@ const style: React.CSSProperties = { } export interface DustbinProps { - accepts: string[] + accept: string[] lastDroppedItem?: any onDrop: (item: any) => void } const Dustbin: React.FC = ({ - accepts, + accept, lastDroppedItem, onDrop, }) => { - const ref = React.useRef(null) - const { isOver, canDrop } = useDrop({ - ref, - type: accepts, - drop: monitor => onDrop(monitor.getItem()), + const [{ isOver, canDrop }, ref] = useDrop({ + accept, + drop: item => onDrop(item), collect: monitor => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), @@ -51,7 +49,7 @@ const Dustbin: React.FC = ({
{isActive ? 'Release to drop' - : `This dustbin accepts: ${accepts.join(', ')}`} + : `This dustbin accepts: ${accept.join(', ')}`} {lastDroppedItem && (

Last dropped: {JSON.stringify(lastDroppedItem)}

diff --git a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/index.tsx b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/index.tsx index 72550605dd..6bf731e92d 100644 --- a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/index.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/index.tsx @@ -51,7 +51,7 @@ export default class Container extends React.Component<{}, ContainerState> {
{dustbins.map(({ accepts, lastDroppedItem }, index) => ( this.handleDrop(index, item)} diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx index c7883ec2cd..e40fd7aef2 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx @@ -23,23 +23,18 @@ export interface BoxProps { } const Box: React.FC = ({ name }) => { - const ref = React.createRef() - const { isDragging } = useDrag({ - ref, - type: ItemTypes.BOX, - begin: () => ({ name }), - end: (monitor: DragSourceMonitor) => { - const item = monitor.getItem() - const dropResult = monitor.getDropResult() + const item = { name, type: ItemTypes.BOX } + const [{ opacity }, ref] = useDrag({ + item, + end: (dropResult?: { name: string }) => { if (dropResult) { alert(`You dropped ${item.name} into ${dropResult.name}!`) } }, collect: (monitor: DragSourceMonitor) => ({ - isDragging: monitor.isDragging(), + opacity: monitor.isDragging() ? 0.4 : 1, }), }) - const opacity = isDragging ? 0.4 : 1 return (
diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx index 33b7255c28..531656d320 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx @@ -19,10 +19,8 @@ const style: React.CSSProperties = { } const Dustbin: React.FC = () => { - const ref = React.useRef(null) - const { isOver, canDrop } = useDrop({ - ref, - type: ItemTypes.BOX, + const [{ isOver, canDrop }, ref] = useDrop({ + accept: ItemTypes.BOX, drop: () => ({ name: 'Dustbin' }), collect: monitor => ({ isOver: monitor.isOver(), diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx index fa45ba3669..ac0eaf583a 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx @@ -20,14 +20,11 @@ interface BoxProps { } const Box: React.FC = ({ name }) => { - const ref = React.useRef(null) - const { isDragging } = useDrag({ - ref, - type: ItemTypes.BOX, - begin: () => ({ name }), - end: monitor => { - const item = monitor.getItem() - const dropResult = monitor.getDropResult() + const item = { name, type: ItemTypes.BOX } + + const [{ isDragging }, ref] = useDrag({ + item, + end: (dropResult?: { name: string }) => { if (dropResult) { alert(`You dropped ${item.name} into ${dropResult.name}!`) } diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx index 24bf0011e5..938208967c 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx @@ -20,10 +20,8 @@ const style: React.CSSProperties = { } const Dustbin: React.FC = () => { - const ref = React.useRef(null) - const { canDrop, isOver } = useDrop({ - ref, - type: ItemTypes.BOX, + const [{ canDrop, isOver }, ref] = useDrop({ + accept: ItemTypes.BOX, drop: () => ({ name: 'Dustbin' }), collect: monitor => ({ isOver: monitor.isOver(), diff --git a/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx index fadb18de52..6e8fcb676f 100644 --- a/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx @@ -22,11 +22,8 @@ export interface BoxProps { } const Box: React.FC = ({ name, type, isDropped }) => { - const ref = React.useRef(null) - const { isDragging } = useDrag({ - ref, - type, - begin: () => ({ name }), + const [{ isDragging }, ref] = useDrag({ + item: { name, type }, isDragging(monitor) { const item = monitor.getItem() return name === item.name diff --git a/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx index 44d29b7edb..29effded52 100644 --- a/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx @@ -26,18 +26,16 @@ export interface DustbinProps { const Dustbin: React.FC = ({ lastDroppedItem, - accepts, + accepts: accept, onDrop, }) => { - const ref = React.useRef(null) - const { isOver, canDrop } = useDrop({ - ref, - type: accepts, + const [{ isOver, canDrop }, ref] = useDrop({ + accept, collect: monitor => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), - drop: monitor => onDrop(monitor.getItem()), + drop: item => onDrop(item), }) const isActive = isOver && canDrop @@ -52,7 +50,7 @@ const Dustbin: React.FC = ({
{isActive ? 'Release to drop' - : `This dustbin accepts: ${accepts.join(', ')}`} + : `This dustbin accepts: ${accept.join(', ')}`} {lastDroppedItem && (

Last dropped: {JSON.stringify(lastDroppedItem)}

diff --git a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx index 2e244d728c..f96920572e 100644 --- a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx @@ -4,6 +4,7 @@ import ItemTypes from './ItemTypes' import DraggableBox from './DraggableBox' import snapToGrid from './snapToGrid' import update from 'immutability-helper' +import { DragItem } from './interfaces' const { useDrop, } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ @@ -46,13 +47,12 @@ const Container: React.FC = props => { const ref = React.useRef(null) useDrop({ ref, - type: ItemTypes.BOX, - drop(monitor) { + accept: ItemTypes.BOX, + drop(item: DragItem, monitor) { const delta = monitor.getDifferenceFromInitialOffset() as { x: number y: number } - const item = monitor.getItem() let left = Math.round(item.left + delta.x) let top = Math.round(item.top + delta.y) @@ -61,6 +61,7 @@ const Container: React.FC = props => { } moveBox(item.id, left, top) + return undefined }, }) diff --git a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx index b488ee0bf9..26375d2544 100644 --- a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx @@ -35,11 +35,9 @@ export interface DraggableBoxProps { const DraggableBox: React.FC = props => { const { id, title, left, top } = props - const ref = React.useRef(null) - - const { isDragging } = useDrag({ - ref, - type: ItemTypes.BOX, + const item = { type: ItemTypes.BOX, id, title, left, top } + const [{ isDragging }, ref] = useDrag({ + item, // Use empty image as a drag preview so browsers don't draw it // and we can draw whatever we want on the custom drag layer instead. preview: getEmptyImage(), @@ -48,7 +46,6 @@ const DraggableBox: React.FC = props => { // when it already knows it's being dragged so we can hide it with CSS. captureDraggingState: true, }, - begin: () => ({ id, title, left, top }), collect: (monitor: DragSourceMonitor) => ({ isDragging: monitor.isDragging(), }), diff --git a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/interfaces.ts b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/interfaces.ts new file mode 100644 index 0000000000..ae7f4fdebe --- /dev/null +++ b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/interfaces.ts @@ -0,0 +1,6 @@ +export interface DragItem { + id: string + type: string + left: number + top: number +} diff --git a/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx b/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx index 2dfcb0b194..87cbfebef6 100644 --- a/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx @@ -28,11 +28,8 @@ const Box: React.FC = ({ hideSourceOnDrag, children, }) => { - const ref = React.useRef(null) - const { isDragging } = useDrag({ - ref, - type: ItemTypes.BOX, - begin: () => ({ id, left, top }), + const [{ isDragging }, ref] = useDrag({ + item: { id, left, top, type: ItemTypes.BOX }, collect: monitor => ({ isDragging: monitor.isDragging(), }), diff --git a/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx b/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx index 924eac674e..89b8a08fae 100644 --- a/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx @@ -6,6 +6,7 @@ import { import ItemTypes from './ItemTypes' import Box from './Box' import update from 'immutability-helper' +import { DragItem } from './interfaces' const { useDrop, @@ -41,13 +42,13 @@ const Container: React.FC = ({ hideSourceOnDrag }) => { const ref = React.useRef(null) useDrop({ ref, - type: ItemTypes.BOX, - drop(monitor) { - const item = monitor.getItem() + accept: ItemTypes.BOX, + drop(item: DragItem, monitor) { const delta = monitor.getDifferenceFromInitialOffset() as XYCoord const left = Math.round(item.left + delta.x) const top = Math.round(item.top + delta.y) moveBox(item.id, left, top) + return undefined }, }) diff --git a/packages/examples-hooks/src/02 Drag Around/Naive/interfaces.ts b/packages/examples-hooks/src/02 Drag Around/Naive/interfaces.ts new file mode 100644 index 0000000000..e899b2ded2 --- /dev/null +++ b/packages/examples-hooks/src/02 Drag Around/Naive/interfaces.ts @@ -0,0 +1,6 @@ +export interface DragItem { + type: string + id: string + top: number + left: number +} diff --git a/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx b/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx index aa03f84c26..a8bb3ea39f 100644 --- a/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx @@ -24,10 +24,8 @@ const SourceBox: React.FC = ({ onToggleForbidDrag, children, }) => { - const ref = React.useRef(null) - const { isDragging } = useDrag({ - ref, - type: `${color}`, + const [{ isDragging }, ref] = useDrag({ + item: { type: `${color}` }, canDrag: () => !forbidDrag, collect: monitor => ({ isDragging: monitor.isDragging(), diff --git a/packages/examples-hooks/src/03 Nesting/Drag Sources/TargetBox.tsx b/packages/examples-hooks/src/03 Nesting/Drag Sources/TargetBox.tsx index 3409e88610..4816372f88 100644 --- a/packages/examples-hooks/src/03 Nesting/Drag Sources/TargetBox.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drag Sources/TargetBox.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' import Colors from './Colors' +import { DragItem } from './interfaces' const { useDrop, @@ -21,12 +22,11 @@ export interface TargetBoxProps { } const TargetBox: React.FC = ({ onDrop, lastDroppedColor }) => { - const ref = React.useRef(null) - const { isOver, draggingColor, canDrop } = useDrop({ - ref, - type: [Colors.YELLOW, Colors.BLUE], - drop(monitor) { - onDrop(monitor.getItemType()) + const [{ isOver, draggingColor, canDrop }, ref] = useDrop({ + accept: [Colors.YELLOW, Colors.BLUE], + drop(item: DragItem) { + onDrop(item.type) + return undefined }, collect: monitor => ({ isOver: monitor.isOver(), diff --git a/packages/examples-hooks/src/03 Nesting/Drag Sources/interfaces.ts b/packages/examples-hooks/src/03 Nesting/Drag Sources/interfaces.ts new file mode 100644 index 0000000000..1d219801b1 --- /dev/null +++ b/packages/examples-hooks/src/03 Nesting/Drag Sources/interfaces.ts @@ -0,0 +1,3 @@ +export interface DragItem { + type: string +} diff --git a/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx b/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx index 518bd2fda4..f8aa094ce3 100644 --- a/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx @@ -15,7 +15,7 @@ const style = { const Box: React.FC = () => { const ref = React.useRef(null) - useDrag({ ref, type: ItemTypes.BOX }) + useDrag({ ref, item: { type: ItemTypes.BOX } }) return (
Drag me diff --git a/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx b/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx index 10673231cb..49e753616b 100644 --- a/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx @@ -34,11 +34,9 @@ const Dustbin: React.FC = ({ greedy, children }) => { const [hasDropped, setHasDropped] = React.useState(false) const [hasDroppedOnChild, setHasDroppedOnChild] = React.useState(false) - const ref = React.useRef(null) - const { isOver, isOverCurrent } = useDrop({ - ref, - type: ItemTypes.BOX, - drop(monitor) { + const [{ isOver, isOverCurrent }, ref] = useDrop({ + accept: ItemTypes.BOX, + drop(item, monitor) { const didDrop = monitor.didDrop() if (didDrop && !greedy) { return diff --git a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx index d1d6ed683b..465de25aab 100644 --- a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx +++ b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx @@ -22,12 +22,8 @@ export interface CardProps { } const Card: React.FC = ({ id, text, moveCard, findCard }) => { - const ref = React.useRef(null) - - const { isDragging } = useDrag({ - ref, - type: ItemTypes.CARD, - begin: () => ({ id, originalIndex: findCard(id).index }), + const [{ isDragging }, ref] = useDrag({ + item: { type: ItemTypes.CARD, id, originalIndex: findCard(id).index }, collect: monitor => ({ isDragging: monitor.isDragging(), }), @@ -35,10 +31,9 @@ const Card: React.FC = ({ id, text, moveCard, findCard }) => { useDrop({ ref, - type: ItemTypes.CARD, + accept: ItemTypes.CARD, canDrop: () => false, - hover(monitor) { - const { id: draggedId } = monitor.getItem() + hover({ id: draggedId }: { id: string }) { if (draggedId !== id) { const { index: overIndex } = findCard(id) moveCard(draggedId, overIndex) diff --git a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx index e8f05bd198..89e3274bc7 100644 --- a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx +++ b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx @@ -49,8 +49,6 @@ const ITEMS = [ const Container: React.FC = () => { const [cards, setCards] = React.useState(ITEMS) - const ref = React.useRef(null) - const moveCard = (id: string, atIndex: number) => { const { card, index } = findCard(id) setCards( @@ -68,7 +66,8 @@ const Container: React.FC = () => { } } - useDrop({ ref, type: ItemTypes.CARD }) + const ref = React.useRef(null) + useDrop({ ref, accept: ItemTypes.CARD }) return ( <>

EXPERIMENTAL API

diff --git a/packages/examples-hooks/src/04 Sortable/Simple/Card.tsx b/packages/examples-hooks/src/04 Sortable/Simple/Card.tsx index b34cc53fc3..a46e800df0 100644 --- a/packages/examples-hooks/src/04 Sortable/Simple/Card.tsx +++ b/packages/examples-hooks/src/04 Sortable/Simple/Card.tsx @@ -25,15 +25,14 @@ export interface CardProps { const Card: React.FC = ({ id, text, index, moveCard }) => { const ref = React.useRef(null) - useDrop({ ref, - type: ItemTypes.CARD, - hover(monitor) { + accept: ItemTypes.CARD, + hover(item: { index: number }, monitor) { if (!ref.current) { return } - const dragIndex = monitor.getItem().index + const dragIndex = item.index const hoverIndex = index // Don't replace items with themselves @@ -75,14 +74,13 @@ const Card: React.FC = ({ id, text, index, moveCard }) => { // Generally it's better to avoid mutations, // but it's good here for the sake of performance // to avoid expensive index searches. - monitor.getItem().index = hoverIndex + item.index = hoverIndex }, }) - const { isDragging } = useDrag({ + const [{ isDragging }] = useDrag({ ref, - type: ItemTypes.CARD, - begin: () => ({ id, index }), + item: { type: ItemTypes.CARD, id, index }, collect: monitor => ({ isDragging: monitor.isDragging(), }), diff --git a/packages/examples-hooks/src/04 Sortable/Simple/index.tsx b/packages/examples-hooks/src/04 Sortable/Simple/index.tsx index c8d7545d7e..32c8c74f44 100644 --- a/packages/examples-hooks/src/04 Sortable/Simple/index.tsx +++ b/packages/examples-hooks/src/04 Sortable/Simple/index.tsx @@ -57,20 +57,22 @@ const Container: React.FC = ({}) => { ) } + const renderCard = (card: { id: number; text: string }, index: number) => { + return ( + + ) + } + return ( <>

EXPERIMENTAL API

-
- {cards.map((card, i) => ( - - ))} -
+
{cards.map((card, i) => renderCard(card, i))}
) } diff --git a/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx b/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx index 4afada2ae1..0cd4a51b63 100644 --- a/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx +++ b/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx @@ -21,11 +21,8 @@ export interface CardProps { } const Card: React.FC = ({ id, text, moveCard }) => { - const ref = React.useRef(null) - const { isDragging } = useDrag({ - ref, - type: ItemTypes.CARD, - begin: () => ({ id }), + const [{ isDragging }, ref] = useDrag({ + item: { id, type: ItemTypes.CARD }, collect: monitor => ({ isDragging: monitor.isDragging(), }), @@ -33,9 +30,8 @@ const Card: React.FC = ({ id, text, moveCard }) => { useDrop({ ref, - type: ItemTypes.CARD, - hover(monitor) { - const draggedId = monitor.getItem().id + accept: ItemTypes.CARD, + hover({ id: draggedId }: { id: string }) { if (draggedId !== id) { moveCard(draggedId, id) } diff --git a/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx b/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx index 297a6151ca..ceabdab30a 100644 --- a/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx +++ b/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx @@ -20,10 +20,8 @@ export interface SourceBoxProps { } const SourceBox: React.FC = ({ showCopyIcon }) => { - const ref = React.useRef(null) - const { opacity } = useDrag({ - ref, - type: ItemTypes.BOX, + const [{ opacity }, ref] = useDrag({ + item: { type: ItemTypes.BOX }, options: { dropEffect: showCopyIcon ? 'copy' : 'move', }, diff --git a/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx b/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx index 80390af211..bc7209d77f 100644 --- a/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx +++ b/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx @@ -15,10 +15,8 @@ const style: React.CSSProperties = { } const TargetBox: React.FC = () => { - const ref = React.useRef(null) - const { canDrop, isOver } = useDrop({ - ref, - type: ItemTypes.BOX, + const [{ canDrop, isOver }, ref] = useDrop({ + accept: ItemTypes.BOX, collect: monitor => ({ canDrop: monitor.canDrop(), isOver: monitor.isOver(), diff --git a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx index e7a3de3b1c..10365615ef 100644 --- a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx +++ b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx @@ -23,11 +23,9 @@ const handleStyle: React.CSSProperties = { } const BoxWithHandle: React.FC = () => { - const ref = React.useRef(null) const preview = React.useRef(null) - const { opacity } = useDrag({ - ref, - type: ItemTypes.BOX, + const [{ opacity }, ref] = useDrag({ + item: { type: ItemTypes.BOX }, preview, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, diff --git a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx index 7f4a6ce448..be8ebb9834 100644 --- a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx +++ b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx @@ -5,6 +5,7 @@ import boxImage from './boxImage' const { useDrag, + useDragPreview, } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ const style = { @@ -16,27 +17,30 @@ const style = { width: '20rem', } -const BoxWithImage: React.FC = () => { - const ref = React.useRef(null) - const preview = new Promise(resolve => { - const img = new Image() - img.onload = () => resolve(img) - img.src = boxImage - }) +const BoxImage = React.forwardRef((props, ref: React.Ref) => { + if (typeof Image === 'undefined') { + return null + } + return +}) - const { opacity } = useDrag({ - ref, - type: ItemTypes.BOX, - preview: preview as any, +const BoxWithImage: React.FC = () => { + const [DragPreview, preview] = useDragPreview(BoxImage) + const [{ opacity }, ref] = useDrag({ + item: { type: ItemTypes.BOX }, + preview, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), }) return ( -
- Drag me to see an image -
+ <> + +
+ Drag me to see an image +
+ ) } export default BoxWithImage diff --git a/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx b/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx index 53b2f64067..2662f524e9 100644 --- a/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx +++ b/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import * as React from 'react' import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__, DropTargetMonitor, @@ -21,12 +21,10 @@ export interface TargetBoxProps { } const TargetBox: React.FC = props => { - const { accepts, onDrop } = props - const ref = useRef(null) - const { canDrop, isOver } = useDrop({ - ref, - type: accepts, - drop(monitor) { + const { accepts: accept, onDrop } = props + const [{ canDrop, isOver }, ref] = useDrop({ + accept, + drop(item, monitor) { if (onDrop) { onDrop(props, monitor) } diff --git a/packages/react-dnd/src/hooks/index.ts b/packages/react-dnd/src/hooks/index.ts index 7fcec0f941..fb5d78554c 100644 --- a/packages/react-dnd/src/hooks/index.ts +++ b/packages/react-dnd/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useDrag' export * from './useDrop' export * from './useDragLayer' +export * from './useDragPreview' diff --git a/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts b/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts index 3a6bac6832..6bd963ed7c 100644 --- a/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts +++ b/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts @@ -1,12 +1,20 @@ import { useMemo, useEffect, useRef } from 'react' import { DragSource, DragDropManager } from 'dnd-core' -import { DragSourceHookSpec, DragSourceMonitor } from '../../interfaces' +import { + DragSourceHookSpec, + DragSourceMonitor, + DragObjectWithType, +} from '../../interfaces' import DragSourceMonitorImpl from '../../DragSourceMonitorImpl' import registerSource from '../../registerSource' -export function useDragSourceMonitor( +export function useDragSourceMonitor< + DragObject extends DragObjectWithType, + DropResult, + CustomProps +>( manager: DragDropManager, - sourceSpec: DragSourceHookSpec, + sourceSpec: DragSourceHookSpec, ): DragSourceMonitor { const sourceSpecRef = useRef(sourceSpec) @@ -18,7 +26,7 @@ export function useDragSourceMonitor( useEffect( function registerSourceWithMonitor() { const { handlerId, unregister } = registerSource( - sourceSpec.type, + sourceSpec.item.type, handler, manager, ) @@ -33,12 +41,11 @@ export function useDragSourceMonitor( () => ({ beginDrag() { - const { begin } = sourceSpecRef.current + const { begin, item } = sourceSpecRef.current if (begin) { - return begin(monitor) - } else { - return {} + begin(monitor) } + return item || {} }, canDrag() { const { canDrag } = sourceSpecRef.current @@ -53,7 +60,7 @@ export function useDragSourceMonitor( endDrag() { const { end } = sourceSpecRef.current if (end) { - end(monitor) + end(monitor.getItem(), monitor) } }, } as DragSource), diff --git a/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts b/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts index 6d59f10914..b5d390a137 100644 --- a/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts +++ b/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts @@ -4,9 +4,9 @@ import { DropTarget, DragDropManager } from 'dnd-core' import DropTargetMonitorImpl from '../../DropTargetMonitorImpl' import registerTarget from '../../registerTarget' -export function useDropTargetMonitor( +export function useDropTargetMonitor( manager: DragDropManager, - targetSpec: DropTargetHookSpec, + targetSpec: DropTargetHookSpec, ): DropTargetMonitor { const targetSpecRef = React.useRef(targetSpec) @@ -20,7 +20,7 @@ export function useDropTargetMonitor( React.useEffect( function registerTargetWithMonitor() { const { handlerId, unregister } = registerTarget( - targetSpec.type, + targetSpec.accept, handler, manager, ) @@ -36,18 +36,18 @@ export function useDropTargetMonitor( ({ canDrop() { const { canDrop } = targetSpecRef.current - return canDrop ? canDrop(monitor) : true + return canDrop ? canDrop(monitor.getItem(), monitor) : true }, hover() { const { hover } = targetSpecRef.current if (hover) { - hover(monitor) + hover(monitor.getItem(), monitor) } }, drop() { const { drop } = targetSpecRef.current if (drop) { - return drop(monitor) + return drop(monitor.getItem(), monitor) } }, } as DropTarget), diff --git a/packages/react-dnd/src/hooks/useDrag.ts b/packages/react-dnd/src/hooks/useDrag.ts index 1573c4a2c0..d41d948c0f 100644 --- a/packages/react-dnd/src/hooks/useDrag.ts +++ b/packages/react-dnd/src/hooks/useDrag.ts @@ -1,6 +1,6 @@ declare var require: any -import { useEffect } from 'react' -import { DragSourceHookSpec } from '../interfaces' +import { useEffect, useRef } from 'react' +import { DragSourceHookSpec, DragObjectWithType } from '../interfaces' import { useDragSourceMonitor } from './internal/useDragSourceMonitor' import { useDragDropManager } from './internal/useDragDropManager' import { Ref, isRef } from './util' @@ -11,22 +11,32 @@ const invariant = require('invariant') * useDragSource hook (This API is experimental and subject to breaking changes in non-major versions) * @param sourceSpec The drag source specification * */ -export function useDrag( - spec: DragSourceHookSpec, -): CustomProps { - const { ref, type, options, preview, previewOptions, collect } = spec - invariant(ref != null, 'ref instance must be defined') - invariant(typeof ref === 'object', 'ref must be a ref object') - invariant(type != null, 'type must be defined') +export function useDrag< + DragObject extends DragObjectWithType, + DropResult, + CollectedProps +>( + spec: DragSourceHookSpec, +): [CollectedProps, React.RefObject] { + const { item, options, preview, previewOptions, collect } = spec + let { ref } = spec + invariant(item != null, 'item must be defined') + invariant(item.type != null, 'item type must be defined') const manager = useDragDropManager() const backend = manager.getBackend() - const monitor = useDragSourceMonitor(manager, spec) + const monitor = useDragSourceMonitor( + manager, + spec, + ) + if (!ref) { + ref = useRef(null) + } /* * Connect the Drag Source Element to the Backend */ useEffect(function connectDragSource() { - const node = ref.current + const node = ref!.current return backend.connectDragSource(monitor.getHandlerId(), node, options) }, []) @@ -35,30 +45,22 @@ export function useDrag( */ useEffect( function connectDragPreview() { - const connectPreview = (p: any) => { - const previewNode = isRef(p) ? (p as Ref).current : p + if (preview) { + const previewNode = isRef(preview) + ? (preview as Ref).current + : preview return backend.connectDragPreview( monitor.getHandlerId(), previewNode, previewOptions, ) } - - if (preview == null) { - return - } - if (typeof (preview as any).then === 'function') { - ;(preview as any).then((p: any) => connectPreview(p)) - } else { - connectPreview(preview) - } }, [preview && (preview as Ref).current], ) - if (collect) { - return useMonitorOutput(monitor as any, collect as any) - } else { - return {} as CustomProps - } + const result: CollectedProps & { ref: React.RefObject } = collect + ? (useMonitorOutput(monitor as any, collect as any) as any) + : (({} as CollectedProps) as any) + return [result, ref] } diff --git a/packages/react-dnd/src/hooks/useDragPreview.ts b/packages/react-dnd/src/hooks/useDragPreview.ts new file mode 100644 index 0000000000..5f383fa9b5 --- /dev/null +++ b/packages/react-dnd/src/hooks/useDragPreview.ts @@ -0,0 +1,26 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' + +/** + * Hook for showing a dragPreview + * @param DragPreview The drag preview component to render + */ +export function useDragPreview( + DragPreview: React.RefForwardingComponent, +): [React.FC, React.RefObject] { + // drag previews won't have layered functionality, so we can create the ref for them + // here + const ref = React.useRef(null) + + // render the dragPreview into a detached element to prevent it from appearing too early + const dragPreviewRoot = document.createElement('div') + const portaledComponent = (props: Props) => { + const sendProps = { ...props, ref } + return createPortal( + React.createElement(DragPreview, sendProps), + dragPreviewRoot, + ) + } + + return [portaledComponent, ref] +} diff --git a/packages/react-dnd/src/hooks/useDrop.ts b/packages/react-dnd/src/hooks/useDrop.ts index 4ac7355e3d..01e9459e78 100644 --- a/packages/react-dnd/src/hooks/useDrop.ts +++ b/packages/react-dnd/src/hooks/useDrop.ts @@ -1,5 +1,5 @@ declare var require: any -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { DropTargetHookSpec } from '../interfaces' import { useDragDropManager } from './internal/useDragDropManager' import { useDropTargetMonitor } from './internal/useDropTargetMonitor' @@ -10,13 +10,15 @@ const invariant = require('invariant') * useDropTarget Hook (This API is experimental and subject to breaking changes in non-breaking versions) * @param spec The drop target specification */ -export function useDrop( - spec: DropTargetHookSpec, -): CustomProps { - const { ref, type, options, collect } = spec - invariant(ref != null, 'ref instance must be defined') - invariant(typeof ref === 'object', 'ref must be a ref object') - invariant(type != null, 'type must be defined') +export function useDrop( + spec: DropTargetHookSpec, +): [CollectedProps, React.RefObject] { + const { accept, options, collect } = spec + invariant(accept != null, 'accept must be defined') + let { ref } = spec + if (!ref) { + ref = useRef(null) + } const manager = useDragDropManager() const backend = manager.getBackend() @@ -26,17 +28,16 @@ export function useDrop( * Connect the Drop Target Element to the Backend */ useEffect(function connectDropTarget() { - if (ref.current) { - const node = ref.current + if (ref!.current) { + const node = ref!.current if (node) { return backend.connectDropTarget(monitor.getHandlerId(), node, options) } } }) - if (collect) { - return useMonitorOutput(monitor as any, collect as any) - } else { - return {} as CustomProps - } + const result: CollectedProps & { ref: React.RefObject } = collect + ? (useMonitorOutput(monitor as any, collect as any) as any) + : (({} as CollectedProps) as any) + return [result, ref] } diff --git a/packages/react-dnd/src/index.ts b/packages/react-dnd/src/index.ts index d0d3148a79..7aa8b8d27b 100644 --- a/packages/react-dnd/src/index.ts +++ b/packages/react-dnd/src/index.ts @@ -8,10 +8,11 @@ export { default as DragLayer } from './DragLayer' export { default as DragSource } from './DragSource' export { default as DropTarget } from './DropTarget' export * from './interfaces' -import { useDrag, useDragLayer, useDrop } from './hooks' +import { useDrag, useDragLayer, useDrop, useDragPreview } from './hooks' export const __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ = { useDrag, useDragLayer, useDrop, + useDragPreview, } diff --git a/packages/react-dnd/src/interfaces/hooksApi.ts b/packages/react-dnd/src/interfaces/hooksApi.ts index a823a23a65..6e01a45fd0 100644 --- a/packages/react-dnd/src/interfaces/hooksApi.ts +++ b/packages/react-dnd/src/interfaces/hooksApi.ts @@ -3,62 +3,49 @@ import { DropTargetMonitor, DragSourceMonitor } from './monitors' import { RefObject } from 'react' import { DragSourceOptions, DragPreviewOptions } from './options' -/** - * Interface for the DropTarget specification object - */ -export interface DropTargetHookSpec { - ref: RefObject - type: TargetType - options?: any - +export interface DragSourceHookSpec< + DragObject extends DragObjectWithType, + DropResult, + CollectedProps +> { /** - * Optional. - * Called when a compatible item is dropped on the target. You may either return undefined, or a plain object. - * If you return an object, it is going to become the drop result and will be available to the drag source in its - * endDrag method as monitor.getDropResult(). This is useful in case you want to perform different actions - * depending on which target received the drop. If you have nested drop targets, you can test whether a nested - * target has already handled drop by checking monitor.didDrop() and monitor.getDropResult(). Both this method and - * the source's endDrag method are good places to fire Flux actions. This method will not be called if canDrop() - * is defined and returns false. + * The ref object to associated with this dragged itom. If this is not specified it will be + * returned in the `ref` field of the result object. */ - drop?: (monitor: DropTargetMonitor) => any + ref?: RefObject /** - * Optional. - * Called when an item is hovered over the component. You can check monitor.isOver({ shallow: true }) to test whether - * the hover happens over just the current target, or over a nested one. Unlike drop(), this method will be called even - * if canDrop() is defined and returns false. You can check monitor.canDrop() to test whether this is the case. + * A plain javascript item describing the data being dragged. + * This is the only information available to the drop targets about the drag + * source so it's important to pick the minimal data they need to know. + * + * You may be tempted to put a reference to the component or complex object here, + * but you shouldx try very hard to avoid doing this because it couples the + * drag sources and drop targets. It's a good idea to use something like + * { id: props.id } + * */ - hover?: (monitor: DropTargetMonitor) => void + item: DragObject /** - * Optional. Use it to specify whether the drop target is able to accept the item. If you want to always allow it, just - * omit this method. Specifying it is handy if you'd like to disable dropping based on some predicate over props or - * monitor.getItem(). Note: You may not call monitor.canDrop() inside this method. + * The drag source options */ - canDrop?: (monitor: DropTargetMonitor) => boolean + options?: DragSourceOptions /** - * A function to collect rendering properties + * An optional dragPreview */ - collect?: (monitor: DropTargetMonitor) => CollectedProps -} + preview?: RefObject | Element -export interface DragSourceHookSpec { - ref: RefObject - type: SourceType - options?: DragSourceOptions - preview?: React.Ref | Element | Promise | Element> + /** + * DragPreview options + */ previewOptions?: DragPreviewOptions /** - * When the dragging starts, beginDrag is called. You must return a plain JavaScript object describing the - * data being dragged. What you return is the only information available to the drop targets about the drag - * source so it's important to pick the minimal data they need to know. You may be tempted to put a reference - * to the component into it, but you should try very hard to avoid doing this because it couples the drag - * sources and drop targets. It's a good idea to return something like { id: props.id } from this method. + * When the dragging starts, beginDrag is called. */ - begin?: (monitor: DragSourceMonitor) => DragObject + begin?: (monitor: DragSourceMonitor) => void /** * Optional. @@ -68,7 +55,7 @@ export interface DragSourceHookSpec { * monitor.getDropResult(). This method is a good place to fire a Flux action. Note: If the component is unmounted while dragging, * component parameter is set to be null. */ - end?: (monitor: DragSourceMonitor) => void + end?: (dropResult: DropResult | undefined, monitor: DragSourceMonitor) => void /** * Optional. @@ -95,3 +82,63 @@ export interface DragSourceHookSpec { */ collect?: (monitor: DragSourceMonitor) => CollectedProps } + +/** + * Interface for the DropTarget specification object + */ +export interface DropTargetHookSpec { + /** + * The ref object to associated with this dragged itom. If this is not specified it will be + * returned in the `ref` field of the result object. + */ + ref?: RefObject + + /** + * The kinds of dragItems this dropTarget accepts + */ + accept: TargetType + + /** + * The drop target optinos + */ + options?: any + + /** + * Optional. + * Called when a compatible item is dropped on the target. You may either return undefined, or a plain object. + * If you return an object, it is going to become the drop result and will be available to the drag source in its + * endDrag method as monitor.getDropResult(). This is useful in case you want to perform different actions + * depending on which target received the drop. If you have nested drop targets, you can test whether a nested + * target has already handled drop by checking monitor.didDrop() and monitor.getDropResult(). Both this method and + * the source's endDrag method are good places to fire Flux actions. This method will not be called if canDrop() + * is defined and returns false. + */ + drop?: ( + item: DragObject, + monitor: DropTargetMonitor, + ) => DropResult | undefined + + /** + * Optional. + * Called when an item is hovered over the component. You can check monitor.isOver({ shallow: true }) to test whether + * the hover happens over just the current target, or over a nested one. Unlike drop(), this method will be called even + * if canDrop() is defined and returns false. You can check monitor.canDrop() to test whether this is the case. + */ + hover?: (item: DragObject, monitor: DropTargetMonitor) => void + + /** + * Optional. Use it to specify whether the drop target is able to accept the item. If you want to always allow it, just + * omit this method. Specifying it is handy if you'd like to disable dropping based on some predicate over props or + * monitor.getItem(). Note: You may not call monitor.canDrop() inside this method. + */ + canDrop?: (item: DragObject, monitor: DropTargetMonitor) => boolean + + /** + * A function to collect rendering properties + */ + collect?: (monitor: DropTargetMonitor) => CollectedProps +} + +export interface DragObjectWithType { + type: SourceType +}