From 891ff214fc9dd735b824168884b202c7023f2d41 Mon Sep 17 00:00:00 2001 From: nicolethoen Date: Thu, 7 Oct 2021 11:50:05 -0400 Subject: [PATCH 1/2] Initial attempts to implement keyboard drag and drop --- .../src/components/DragDrop/DragButton.tsx | 27 ++ .../src/components/DragDrop/DragDrop.tsx | 17 +- .../src/components/DragDrop/Draggable.tsx | 232 ++++++++++++++---- .../components/Toolbar/ToolbarToggleGroup.tsx | 2 +- 4 files changed, 221 insertions(+), 57 deletions(-) create mode 100644 packages/react-core/src/components/DragDrop/DragButton.tsx diff --git a/packages/react-core/src/components/DragDrop/DragButton.tsx b/packages/react-core/src/components/DragDrop/DragButton.tsx new file mode 100644 index 00000000000..7e8d5db9e9c --- /dev/null +++ b/packages/react-core/src/components/DragDrop/DragButton.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import GripVerticalIcon from '@patternfly/react-icons/dist/esm/icons/grip-vertical-icon'; +import { Button, ButtonVariant } from '../Button'; +import { DraggableContext } from './Draggable'; + +interface DragButtonProps { + /** Accessible label for the button */ + 'aria-label'?: string; +} + +export const DragButton: React.FunctionComponent = ({ + 'aria-label': ariaLabel, + ...props +}: DragButtonProps) => { + const { setHasDragButtons, onKeyDown } = React.useContext(DraggableContext); + + React.useEffect(() => { + setHasDragButtons(true); + }, []); + + return ( + + ); +}; +DragButton.displayName = 'DragButton'; diff --git a/packages/react-core/src/components/DragDrop/DragDrop.tsx b/packages/react-core/src/components/DragDrop/DragDrop.tsx index b2dd873b2f1..86973bc4a01 100644 --- a/packages/react-core/src/components/DragDrop/DragDrop.tsx +++ b/packages/react-core/src/components/DragDrop/DragDrop.tsx @@ -10,7 +10,9 @@ interface DraggableItemPosition { export const DragDropContext = React.createContext({ onDrag: (_source: DraggableItemPosition) => true as boolean, onDragMove: (_source: DraggableItemPosition, _dest?: DraggableItemPosition) => {}, - onDrop: (_source: DraggableItemPosition, _dest?: DraggableItemPosition) => false as boolean + onDrop: (_source: DraggableItemPosition, _dest?: DraggableItemPosition) => false as boolean, + isDragging: false, + setIsDragging: (isDragging: boolean) => {} }); interface DragDropProps { @@ -28,8 +30,13 @@ export const DragDrop: React.FunctionComponent = ({ children, onDrag = () => true, onDragMove = () => {}, - onDrop = () => false -}: DragDropProps) => ( - {children} -); + onDrop = () => false, + +}: DragDropProps) => { + const [isDragging, setIsDragging] = React.useState(false); + + return ( + {children} + ); +}; DragDrop.displayName = 'DragDrop'; diff --git a/packages/react-core/src/components/DragDrop/Draggable.tsx b/packages/react-core/src/components/DragDrop/Draggable.tsx index 30a3438018b..59a99075330 100644 --- a/packages/react-core/src/components/DragDrop/Draggable.tsx +++ b/packages/react-core/src/components/DragDrop/Draggable.tsx @@ -4,6 +4,11 @@ import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; import { DroppableContext } from './DroppableContext'; import { DragDropContext } from './DragDrop'; +export const DraggableContext = React.createContext({ + setHasDragButtons: (hasDragButtons: boolean) => {}, + onKeyDown: (e: React.KeyboardEvent) => {} +}); + export interface DraggableProps extends React.HTMLProps { /** Content rendered inside DragDrop */ children?: React.ReactNode; @@ -85,17 +90,15 @@ export const Draggable: React.FunctionComponent = ({ /* eslint-disable prefer-const */ let [style, setStyle] = React.useState(styleProp); /* eslint-enable prefer-const */ - const [isDragging, setIsDragging] = React.useState(false); + const [isDraggingUsingMouse, setIsDraggingUsingMouse] = React.useState(false); + const [isDraggingUsingKeyboard, setIsDraggingUsingKeyboard] = React.useState(false); const [isValidDrag, setIsValidDrag] = React.useState(true); + const [hasDragButtons, setHasDragButtons] = React.useState(false); const { zone, droppableId } = React.useContext(DroppableContext); - const { onDrag, onDragMove, onDrop } = React.useContext(DragDropContext); + const { onDrag, onDragMove, onDrop, isDragging, setIsDragging } = React.useContext(DragDropContext); // Some state is better just to leave as vars passed around between various callbacks // You can only drag around one item at a time anyways... - let startX = 0; - let startY = 0; let index: number = null; // Index of this draggable - let hoveringDroppable: HTMLElement; - let hoveringIndex: number = null; let mouseMoveListener: EventListener; let mouseUpListener: EventListener; // Makes it so dragging the _bottom_ of the item over the halfway of another moves it @@ -103,13 +106,15 @@ export const Draggable: React.FunctionComponent = ({ // After item returning to where it started animation completes const onTransitionEnd = (_ev: React.TransitionEvent) => { - if (isDragging) { + if (isDraggingUsingKeyboard || isDraggingUsingMouse) { + setIsDraggingUsingKeyboard(false); + setIsDraggingUsingMouse(false); setIsDragging(false); setStyle(styleProp); } }; - function getSourceAndDest() { + function getSourceAndDest(hoveringIndex: number, hoveringDroppable: HTMLElement) { const hoveringDroppableId = hoveringDroppable ? hoveringDroppable.getAttribute('data-pf-droppableid') : null; const source = { droppableId, @@ -118,21 +123,22 @@ export const Draggable: React.FunctionComponent = ({ const dest = hoveringDroppableId !== null && hoveringIndex !== null ? { - droppableId: hoveringDroppableId, - index: hoveringIndex - } + droppableId: hoveringDroppableId, + index: hoveringIndex + } : undefined; return { source, dest, hoveringDroppableId }; } - const onMouseUpWhileDragging = (droppableItems: DroppableItem[]) => { + const onMouseUpWhileDragging = (droppableItems: DroppableItem[], hoveringIndex: number, hoveringDroppable: HTMLElement) => { droppableItems.forEach(resetDroppableItem); document.removeEventListener('mousemove', mouseMoveListener); document.removeEventListener('mouseup', mouseUpListener); document.removeEventListener('contextmenu', mouseUpListener); - const { source, dest, hoveringDroppableId } = getSourceAndDest(); + const { source, dest, hoveringDroppableId } = getSourceAndDest(hoveringIndex, hoveringDroppable); const consumerReordered = onDrop(source, dest); if (consumerReordered && droppableId === hoveringDroppableId) { + setIsDraggingUsingMouse(false); setIsDragging(false); setStyle(styleProp); } else if (!consumerReordered) { @@ -148,15 +154,16 @@ export const Draggable: React.FunctionComponent = ({ }; // This is where the magic happens - const onMouseMoveWhileDragging = (ev: MouseEvent, droppableItems: DroppableItem[], blankDivRect: DOMRect) => { + const onMouseMoveWhileDragging = (ev: MouseEvent, droppableItems: DroppableItem[], blankDivRect: DOMRect, startingCoordinates?: {startX: number, startY: number}) => { + const { startX, startY } = startingCoordinates; // Compute each time what droppable node we are hovering over - hoveringDroppable = null; + let newHoveringDroppable = null as HTMLElement; droppableItems.forEach(droppableItem => { const { node, rect, isDraggingHost, draggableNodes, draggableNodesRects } = droppableItem; if (overlaps(ev, rect)) { // Add valid dropzone style node.classList.remove(styles.modifiers.dragOutside); - hoveringDroppable = node; + newHoveringDroppable = node; // Check if we need to add a blank div row if (node.getAttribute('blankDiv') !== 'true' && !isDraggingHost) { const blankDiv = document.createElement('div'); @@ -193,18 +200,15 @@ export const Draggable: React.FunctionComponent = ({ node.classList.add(styles.modifiers.dragOutside); } }); - - // Move hovering draggable and style it based on cursor position setStyle({ ...style, - transform: `translate(${ev.pageX - startX}px, ${ev.pageY - startY}px)` + transform: `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)` }); - setIsValidDrag(Boolean(hoveringDroppable)); + setIsValidDrag(Boolean(newHoveringDroppable)); - // Iterate through sibling draggable nodes to reposition them and store correct hoveringIndex for onDrop - hoveringIndex = null; - if (hoveringDroppable) { - const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === hoveringDroppable); + let newHoveringIndex: number = null; + if (newHoveringDroppable) { + const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === newHoveringDroppable); let lastTranslate = 0; draggableNodes.forEach((n, i) => { n.style.transition = 'transform 0.5s cubic-bezier(0.2, 1, 0.1, 1) 0s'; @@ -212,34 +216,26 @@ export const Draggable: React.FunctionComponent = ({ const halfway = rect.y + rect.height / 2; let translateY = 0; // Use offset for more interactive translations - if (startY < halfway && ev.pageY + (blankDivRect.height - startYOffset) > halfway) { + if (startY < halfway && ev.clientY + (blankDivRect.height - startYOffset) > halfway) { translateY -= blankDivRect.height; - } else if (startY >= halfway && ev.pageY - startYOffset <= halfway) { + } else if (startY >= halfway && ev.clientY - startYOffset <= halfway) { translateY += blankDivRect.height; } // Clever way to find item currently hovering over if ((translateY <= lastTranslate && translateY < 0) || (translateY > lastTranslate && translateY > 0)) { - hoveringIndex = i; + newHoveringIndex = i; } n.style.transform = `translate(0, ${translateY}px`; lastTranslate = translateY; }); } - - const { source, dest } = getSourceAndDest(); + const { source, dest } = getSourceAndDest(newHoveringIndex, newHoveringDroppable); onDragMove(source, dest); + mouseUpListener = () => onMouseUpWhileDragging(droppableItems, newHoveringIndex, newHoveringDroppable); + document.addEventListener('mouseup', mouseUpListener); }; - const onDragStart = (ev: React.DragEvent) => { - // Default HTML drag and drop doesn't allow us to change what the thing - // being dragged looks like. Because of this we'll use prevent the default - // and use `mouseMove` and `mouseUp` instead - ev.preventDefault(); - if (isDragging) { - // still in animation - return; - } - + const calculateBounds = (ev: React.DragEvent | React.KeyboardEvent) => { // Cache droppable and draggable nodes and their bounding rects const dragging = ev.target as HTMLElement; const rect = dragging.getBoundingClientRect(); @@ -263,6 +259,24 @@ export const Draggable: React.FunctionComponent = ({ return acc; }, []); + return { + rect, + droppableItems + }; + }; + + const onDragStart = (ev: React.DragEvent) => { + // Default HTML drag and drop doesn't allow us to change what the thing + // being dragged looks like. Because of this we'll use prevent the default + // and use `mouseMove` and `mouseUp` instead + ev.preventDefault(); + if (isDragging) { + // still in animation + return; + } + + const { droppableItems, rect } = calculateBounds(ev); + if (!onDrag({ droppableId, index })) { // Consumer disallowed drag return; @@ -275,43 +289,159 @@ export const Draggable: React.FunctionComponent = ({ left: rect.x, width: rect.width, height: rect.height, - '--pf-c-draggable--m-dragging--BackgroundColor': getInheritedBackgroundColor(dragging), + '--pf-c-draggable--m-dragging--BackgroundColor': getInheritedBackgroundColor(ev.target as HTMLElement), position: 'fixed', zIndex: 5000 } as any; setStyle(style); // Store event details - startX = ev.pageX; - startY = ev.pageY; - startYOffset = startY - rect.y; + startYOffset = ev.clientY - rect.y; + setIsDraggingUsingMouse(true); setIsDragging(true); - mouseMoveListener = ev => onMouseMoveWhileDragging(ev as MouseEvent, droppableItems, rect); - mouseUpListener = () => onMouseUpWhileDragging(droppableItems); + mouseMoveListener = e => onMouseMoveWhileDragging(e as MouseEvent, droppableItems, rect, {startX: ev.clientX, startY: ev.clientY}); document.addEventListener('mousemove', mouseMoveListener); - document.addEventListener('mouseup', mouseUpListener); // Comment out this line to debug while dragging by right clicking // document.addEventListener('contextmenu', mouseUpListener); }; + const endDragUsingKeyboard = (cancel: boolean) => { + if (isDraggingUsingKeyboard) { + setStyle(styleProp); + const droppableNodes = Array.from(document.querySelectorAll(`[data-pf-droppable="${zone}"]`)) as HTMLElement[]; + droppableNodes.forEach(node => { + node.classList.remove(styles.modifiers.dragging); + }); + + setIsDraggingUsingKeyboard(false); + setIsDragging(false); + + if (cancel) { + onDrop({ droppableId, index }, undefined); + // Animate item returning to where it started + setStyle({ + ...style, + transition: 'transform 0.5s cubic-bezier(0.2, 1, 0.1, 1) 0s', + transform: '', + background: styleProp.background, + boxShadow: styleProp.boxShadow + }); + } + } + }; + + React.useEffect(() => { + document.addEventListener('click', () => endDragUsingKeyboard(true)); + return () => { + document.removeEventListener('click', () => endDragUsingKeyboard(true)); + } + }, [isDraggingUsingKeyboard]); + + const onKeyDown = (e: React.KeyboardEvent) => { + if (isDraggingUsingKeyboard) { + if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape' || e.key === 'Tab') { + setStyle(styleProp); + const droppableNodes = Array.from(document.querySelectorAll(`[data-pf-droppable="${zone}"]`)) as HTMLElement[]; + droppableNodes.forEach(node => { + node.classList.remove(styles.modifiers.dragging); + }); + + setIsDraggingUsingKeyboard(false); + setIsDragging(false); + if (e.key === ' ' || e.key === 'Tab') { + e.preventDefault(); + } + if (e.key === 'Escape') { + endDragUsingKeyboard(true); + } else { + // don't cancel + const { droppableItems } = calculateBounds(e); + let hoveringIndex = null as number; + let hoveringDroppable = null as HTMLElement; + droppableItems.forEach(droppableItem => { + const { node, rect } = droppableItem; + const fakeMouseEvent = new MouseEvent("mousemove", { clientX: rect.x + 1, clientY: rect.y }); + if (overlaps(fakeMouseEvent, rect)) { + hoveringDroppable = node + } + }); + const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === hoveringDroppable); + let lastTranslate = 0; + draggableNodes.forEach((n: HTMLElement, i: number) => { + n.style.transition = 'transform 0.5s cubic-bezier(0.2, 1, 0.1, 1) 0s'; + const rect = draggableNodesRects[i]; + const halfway = rect.y + rect.height / 2; + let translateY = 0; + // Use offset for more interactive translations + if (rect.y < halfway && rect.y + (rect.height - startYOffset) > halfway) { + translateY -= rect.height; + } else if (rect.y >= halfway && rect.y - startYOffset <= halfway) { + translateY += rect.height; + } + // Clever way to find item currently hovering over + if ((translateY <= lastTranslate && translateY < 0) || (translateY > lastTranslate && translateY > 0)) { + hoveringIndex = i; + } + n.style.transform = `translate(0, ${translateY}px`; + lastTranslate = translateY; + }); + onMouseUpWhileDragging(droppableItems, hoveringIndex, hoveringDroppable); + } + } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + const { droppableItems, rect } = calculateBounds(e); + const newClientY = e.key === 'ArrowUp' ? rect.y - rect.height : rect.y + rect.height; + const fakeMouseEvent = new MouseEvent("mousemove", { clientX: rect.x + 1, clientY: newClientY }); + onMouseMoveWhileDragging(fakeMouseEvent, droppableItems, rect, {startX: rect.x, startY: rect.y}); + e.preventDefault(); + } + } else if (!isDragging) { + if (e.key === 'Enter' || e.key === ' ') { + if (e.key === ' ') { + e.preventDefault(); + } + + setIsDraggingUsingKeyboard(true); + setIsDragging(true); + // Cache droppable and draggable nodes and their bounding rects + const { rect } = calculateBounds(e); + + if (!onDrag({ droppableId, index })) { + // Consumer disallowed drag + return; + } + + // Set initial style so future style mods take effect + style = { + ...style, + width: rect.width, + height: rect.height, + '--pf-c-draggable--m-dragging--BackgroundColor': getInheritedBackgroundColor(e.target as HTMLElement), + zIndex: 5000 + } as any; + setStyle(style); + } + } + }; + const childProps = { - 'data-pf-draggable-zone': isDragging ? null : zone, + 'data-pf-draggable-zone': isDraggingUsingMouse || isDraggingUsingKeyboard ? null : zone, draggable: true, className: css( styles.draggable, - isDragging && styles.modifiers.dragging, + (isDraggingUsingMouse || isDraggingUsingKeyboard) && styles.modifiers.dragging, !isValidDrag && styles.modifiers.dragOutside, className ), onDragStart, onTransitionEnd, style, + ...(!hasDragButtons && {onKeyDown, tabIndex: 0}), ...props }; return ( - + {/* Leave behind blank spot per-design */} - {isDragging && ( + {isDraggingUsingMouse && (
{children}
@@ -321,7 +451,7 @@ export const Draggable: React.FunctionComponent = ({ ) : (
{children}
)} -
+ ); }; Draggable.displayName = 'Draggable'; diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index f23a3dd658d..d4465ce2a95 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -9,7 +9,7 @@ import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/global_breakpo import { formatBreakpointMods, toCamel, capitalize, canUseDOM } from '../../helpers/util'; export interface ToolbarToggleGroupProps extends ToolbarGroupProps { - /** An icon to be rendered when the toggle group has collapsed down */ + /** Content to be rendered when the toggle group has collapsed down */ toggleIcon: React.ReactNode; /** Controls when filters are shown and when the toggle button is hidden. */ breakpoint: 'md' | 'lg' | 'xl' | '2xl'; From e449361e6f30a37480b2a47b91ec229e90ace93a Mon Sep 17 00:00:00 2001 From: nicolethoen Date: Thu, 18 Nov 2021 14:37:57 -0500 Subject: [PATCH 2/2] dropping Draggable items still isn't working --- .../src/components/DragDrop/Draggable.tsx | 99 +++++++++++++++---- 1 file changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/react-core/src/components/DragDrop/Draggable.tsx b/packages/react-core/src/components/DragDrop/Draggable.tsx index 59a99075330..80ec5156358 100644 --- a/packages/react-core/src/components/DragDrop/Draggable.tsx +++ b/packages/react-core/src/components/DragDrop/Draggable.tsx @@ -5,7 +5,9 @@ import { DroppableContext } from './DroppableContext'; import { DragDropContext } from './DragDrop'; export const DraggableContext = React.createContext({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars setHasDragButtons: (hasDragButtons: boolean) => {}, + // eslint-disable-next-line @typescript-eslint/no-unused-vars onKeyDown: (e: React.KeyboardEvent) => {} }); @@ -101,6 +103,7 @@ export const Draggable: React.FunctionComponent = ({ let index: number = null; // Index of this draggable let mouseMoveListener: EventListener; let mouseUpListener: EventListener; + let hasMouseUpListener: boolean = false; // Makes it so dragging the _bottom_ of the item over the halfway of another moves it let startYOffset = 0; @@ -123,19 +126,60 @@ export const Draggable: React.FunctionComponent = ({ const dest = hoveringDroppableId !== null && hoveringIndex !== null ? { - droppableId: hoveringDroppableId, - index: hoveringIndex - } + droppableId: hoveringDroppableId, + index: hoveringIndex + } : undefined; return { source, dest, hoveringDroppableId }; } - const onMouseUpWhileDragging = (droppableItems: DroppableItem[], hoveringIndex: number, hoveringDroppable: HTMLElement) => { + const onMouseUpWhileDragging = ( + ev: MouseEvent, + droppableItems: DroppableItem[], + hoveringIndex: number, + hoveringDroppable: HTMLElement, + blankDivRect: DOMRect, + startY: number + ) => { + // Compute each time what droppable node we are hovering over + let newHoveringDroppable = null as HTMLElement; + droppableItems.forEach(droppableItem => { + const { node, rect } = droppableItem; + if (overlaps(ev, rect)) { + newHoveringDroppable = node; + } + }); + setIsValidDrag(Boolean(newHoveringDroppable)); + + let newHoveringIndex: number = null; + if (newHoveringDroppable) { + const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === newHoveringDroppable); + const lastTranslate = 0; + draggableNodes.forEach((n, i) => { + const rect = draggableNodesRects[i]; + const halfway = rect.y + rect.height / 2; + let translateY = 0; + // Use offset for more interactive translations + if (startY < halfway && ev.clientY + (blankDivRect.height - startYOffset) > halfway) { + translateY -= blankDivRect.height; + } else if (startY >= halfway && ev.clientY - startYOffset <= halfway) { + translateY += blankDivRect.height; + } + // Clever way to find item currently hovering over + if ((translateY <= lastTranslate && translateY < 0) || (translateY > lastTranslate && translateY > 0)) { + newHoveringIndex = i; + } + }); + } + droppableItems.forEach(resetDroppableItem); document.removeEventListener('mousemove', mouseMoveListener); document.removeEventListener('mouseup', mouseUpListener); document.removeEventListener('contextmenu', mouseUpListener); - const { source, dest, hoveringDroppableId } = getSourceAndDest(hoveringIndex, hoveringDroppable); + const { source, dest, hoveringDroppableId } = getSourceAndDest( + newHoveringIndex || hoveringIndex, + newHoveringDroppable || hoveringDroppable + ); const consumerReordered = onDrop(source, dest); if (consumerReordered && droppableId === hoveringDroppableId) { setIsDraggingUsingMouse(false); @@ -154,7 +198,12 @@ export const Draggable: React.FunctionComponent = ({ }; // This is where the magic happens - const onMouseMoveWhileDragging = (ev: MouseEvent, droppableItems: DroppableItem[], blankDivRect: DOMRect, startingCoordinates?: {startX: number, startY: number}) => { + const onMouseMoveWhileDragging = ( + ev: MouseEvent, + droppableItems: DroppableItem[], + blankDivRect: DOMRect, + startingCoordinates?: { startX: number; startY: number } + ) => { const { startX, startY } = startingCoordinates; // Compute each time what droppable node we are hovering over let newHoveringDroppable = null as HTMLElement; @@ -231,11 +280,22 @@ export const Draggable: React.FunctionComponent = ({ } const { source, dest } = getSourceAndDest(newHoveringIndex, newHoveringDroppable); onDragMove(source, dest); - mouseUpListener = () => onMouseUpWhileDragging(droppableItems, newHoveringIndex, newHoveringDroppable); - document.addEventListener('mouseup', mouseUpListener); + if (!hasMouseUpListener) { + mouseUpListener = e => + onMouseUpWhileDragging( + e as MouseEvent, + droppableItems, + newHoveringIndex, + newHoveringDroppable, + blankDivRect, + startY + ); + document.addEventListener('mouseup', mouseUpListener); + hasMouseUpListener = true; + } }; - const calculateBounds = (ev: React.DragEvent | React.KeyboardEvent) => { + const calculateBounds = (ev: React.DragEvent | React.KeyboardEvent | MouseEvent) => { // Cache droppable and draggable nodes and their bounding rects const dragging = ev.target as HTMLElement; const rect = dragging.getBoundingClientRect(); @@ -298,7 +358,8 @@ export const Draggable: React.FunctionComponent = ({ startYOffset = ev.clientY - rect.y; setIsDraggingUsingMouse(true); setIsDragging(true); - mouseMoveListener = e => onMouseMoveWhileDragging(e as MouseEvent, droppableItems, rect, {startX: ev.clientX, startY: ev.clientY}); + mouseMoveListener = e => + onMouseMoveWhileDragging(e as MouseEvent, droppableItems, rect, { startX: ev.clientX, startY: ev.clientY }); document.addEventListener('mousemove', mouseMoveListener); // Comment out this line to debug while dragging by right clicking // document.addEventListener('contextmenu', mouseUpListener); @@ -333,7 +394,7 @@ export const Draggable: React.FunctionComponent = ({ document.addEventListener('click', () => endDragUsingKeyboard(true)); return () => { document.removeEventListener('click', () => endDragUsingKeyboard(true)); - } + }; }, [isDraggingUsingKeyboard]); const onKeyDown = (e: React.KeyboardEvent) => { @@ -359,9 +420,9 @@ export const Draggable: React.FunctionComponent = ({ let hoveringDroppable = null as HTMLElement; droppableItems.forEach(droppableItem => { const { node, rect } = droppableItem; - const fakeMouseEvent = new MouseEvent("mousemove", { clientX: rect.x + 1, clientY: rect.y }); + const fakeMouseEvent = new MouseEvent('mousemove', { clientX: rect.x + 1, clientY: rect.y }); if (overlaps(fakeMouseEvent, rect)) { - hoveringDroppable = node + hoveringDroppable = node; } }); const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === hoveringDroppable); @@ -384,13 +445,15 @@ export const Draggable: React.FunctionComponent = ({ n.style.transform = `translate(0, ${translateY}px`; lastTranslate = translateY; }); - onMouseUpWhileDragging(droppableItems, hoveringIndex, hoveringDroppable); + const { rect } = calculateBounds(e); + const fakeMouseEvent = new MouseEvent('mousemove', { clientX: rect.x + 1, clientY: rect.y }); + onMouseUpWhileDragging(fakeMouseEvent, droppableItems, hoveringIndex, hoveringDroppable, rect, rect.y); } } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { const { droppableItems, rect } = calculateBounds(e); const newClientY = e.key === 'ArrowUp' ? rect.y - rect.height : rect.y + rect.height; - const fakeMouseEvent = new MouseEvent("mousemove", { clientX: rect.x + 1, clientY: newClientY }); - onMouseMoveWhileDragging(fakeMouseEvent, droppableItems, rect, {startX: rect.x, startY: rect.y}); + const fakeMouseEvent = new MouseEvent('mousemove', { clientX: rect.x + 1, clientY: newClientY }); + onMouseMoveWhileDragging(fakeMouseEvent, droppableItems, rect, { startX: rect.x, startY: rect.y }); e.preventDefault(); } } else if (!isDragging) { @@ -434,12 +497,12 @@ export const Draggable: React.FunctionComponent = ({ onDragStart, onTransitionEnd, style, - ...(!hasDragButtons && {onKeyDown, tabIndex: 0}), + ...(!hasDragButtons && { onKeyDown, tabIndex: 0 }), ...props }; return ( - + {/* Leave behind blank spot per-design */} {isDraggingUsingMouse && (