Add custom drag context wrapper to inject the overlay rect on drag end

This commit is contained in:
Mitchell McCaffrey 2021-06-10 18:04:32 +10:00
parent 5727bade36
commit b75db97c26
4 changed files with 94 additions and 32 deletions

View File

@ -1,10 +1,9 @@
import React, { useState, useRef } from "react"; import React, { useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Box, Flex } from "theme-ui"; import { Box, Flex } from "theme-ui";
import SimpleBar from "simplebar-react"; import SimpleBar from "simplebar-react";
import { import {
DragOverlay, DragOverlay,
DndContext,
MouseSensor, MouseSensor,
TouchSensor, TouchSensor,
KeyboardSensor, KeyboardSensor,
@ -24,6 +23,7 @@ import usePreventSelect from "../../hooks/usePreventSelect";
import { useTokenData } from "../../contexts/TokenDataContext"; import { useTokenData } from "../../contexts/TokenDataContext";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { useMapStage } from "../../contexts/MapStageContext"; import { useMapStage } from "../../contexts/MapStageContext";
import DragContext from "../../contexts/DragContext";
import { import {
createTokenState, createTokenState,
@ -40,10 +40,6 @@ function TokenBar({ onMapTokensStateCreate }) {
const [dragId, setDragId] = useState(); const [dragId, setDragId] = useState();
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
// Use a ref to the drag overlay to get it's position on dragEnd
// TODO: use active.rect when dnd-kit bug is fixed
// https://github.com/clauderic/dnd-kit/issues/238
const dragOverlayRef = useRef();
const mouseSensor = useSensor(MouseSensor, { const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 5 }, activationConstraint: { distance: 5 },
@ -61,13 +57,12 @@ function TokenBar({ onMapTokensStateCreate }) {
preventSelect(); preventSelect();
} }
function handleDragEnd({ active }) { function handleDragEnd({ active, overlayNodeClientRect }) {
setDragId(null); setDragId(null);
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
const dragOverlay = dragOverlayRef.current; if (mapStage) {
if (mapStage && dragOverlay) { const dragRect = overlayNodeClientRect;
const dragRect = dragOverlay.getBoundingClientRect();
const dragPosition = { const dragPosition = {
x: dragRect.left + dragRect.width / 2, x: dragRect.left + dragRect.width / 2,
y: dragRect.top + dragRect.height / 2, y: dragRect.top + dragRect.height / 2,
@ -146,7 +141,7 @@ function TokenBar({ onMapTokensStateCreate }) {
} }
return ( return (
<DndContext <DragContext
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel} onDragCancel={handleDragCancel}
@ -185,24 +180,13 @@ function TokenBar({ onMapTokensStateCreate }) {
<SelectTokensButton onMapTokensStateCreate={onMapTokensStateCreate} /> <SelectTokensButton onMapTokensStateCreate={onMapTokensStateCreate} />
</Flex> </Flex>
{createPortal( {createPortal(
<DragOverlay <DragOverlay dropAnimation={null}>
// Ensure a drop animation plays to allow us to get the position of the drag overlay in drag end {dragId && renderToken(findGroup(tokenGroups, dragId), false)}
dropAnimation={{
dragSourceOpacity: 0,
duration: 1,
easing: "ease",
}}
>
{dragId && (
<div ref={dragOverlayRef}>
{renderToken(findGroup(tokenGroups, dragId), false)}
</div>
)}
</DragOverlay>, </DragOverlay>,
document.body document.body
)} )}
</Box> </Box>
</DndContext> </DragContext>
); );
} }

View File

@ -0,0 +1,75 @@
// eslint-disable-next-line no-unused-vars
import React, { useRef, ReactNode } from "react";
import {
DndContext,
useDndContext,
useDndMonitor,
// eslint-disable-next-line no-unused-vars
DragEndEvent,
} from "@dnd-kit/core";
/**
* Wrap a dnd-kit DndContext with a position monitor to get the
* active drag element on drag end
* TODO: use look into fixing this upstream
* Related: https://github.com/clauderic/dnd-kit/issues/238
*/
/**
* @typedef DragEndOverlayEvent
* @property {DOMRect} overlayNodeClientRect
*
* @typedef {DragEndEvent & DragEndOverlayEvent} DragEndWithOverlayProps
*/
/**
* @callback DragEndWithOverlayEvent
* @param {DragEndWithOverlayProps} props
*/
/**
* @typedef CustomDragProps
* @property {DragEndWithOverlayEvent=} onDragEnd
* @property {ReactNode} children
*/
/**
* @param {CustomDragProps} props
*/
function DragPositionMonitor({ children, onDragEnd }) {
const { overlayNode } = useDndContext();
const overlayNodeClientRectRef = useRef();
function handleDragMove() {
if (overlayNode?.nodeRef?.current) {
overlayNodeClientRectRef.current = overlayNode.nodeRef.current.getBoundingClientRect();
}
}
function handleDragEnd(props) {
onDragEnd &&
onDragEnd({
...props,
overlayNodeClientRect: overlayNodeClientRectRef.current,
});
}
useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove });
return children;
}
/**
* TODO: Import Props interface from dnd-kit with conversion to Typescript
* @param {CustomDragProps} props
*/
function DragContext({ children, onDragEnd, ...props }) {
return (
<DndContext {...props}>
<DragPositionMonitor onDragEnd={onDragEnd}>
{children}
</DragPositionMonitor>
</DndContext>
);
}
export default DragContext;

View File

@ -1,6 +1,5 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { import {
DndContext,
MouseSensor, MouseSensor,
TouchSensor, TouchSensor,
KeyboardSensor, KeyboardSensor,
@ -9,6 +8,8 @@ import {
closestCenter, closestCenter,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import DragContext from "./DragContext";
import { useGroup } from "./GroupContext"; import { useGroup } from "./GroupContext";
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
@ -108,7 +109,7 @@ export function TileDragProvider({
} }
function handleDragEnd(event) { function handleDragEnd(event) {
const { active, over } = event; const { active, over, overlayNodeClientRect } = event;
setDragId(); setDragId();
setOverId(); setOverId();
@ -143,7 +144,9 @@ export function TileDragProvider({
} }
onGroupsChange(newGroups); onGroupsChange(newGroups);
} else if (over.id === ADD_TO_MAP_ID) { } else if (over.id === ADD_TO_MAP_ID) {
onDragAdd && onDragAdd(selectedGroupIds, over.rect); onDragAdd &&
overlayNodeClientRect &&
onDragAdd(selectedGroupIds, overlayNodeClientRect);
} else if (!filter) { } else if (!filter) {
// Hanlde tile move only if we have no filter // Hanlde tile move only if we have no filter
const overGroupIndex = activeGroups.findIndex( const overGroupIndex = activeGroups.findIndex(
@ -210,7 +213,7 @@ export function TileDragProvider({
const value = { dragId, overId, dragCursor }; const value = { dragId, overId, dragCursor };
return ( return (
<DndContext <DragContext
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={handleDragOver} onDragOver={handleDragOver}
@ -221,7 +224,7 @@ export function TileDragProvider({
<TileDragContext.Provider value={value}> <TileDragContext.Provider value={value}>
{children} {children}
</TileDragContext.Provider> </TileDragContext.Provider>
</DndContext> </DragContext>
); );
} }

View File

@ -144,8 +144,8 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) {
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
function handleTokensAddToMap(groupIds, rect) { function handleTokensAddToMap(groupIds, rect) {
let clientPosition = new Vector2( let clientPosition = new Vector2(
rect.width / 2 + rect.offsetLeft, rect.width / 2 + rect.left,
rect.height / 2 + rect.offsetTop rect.height / 2 + rect.top
); );
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
if (!mapStage) { if (!mapStage) {