From ae68082a5b296f9492713a049c6f2cf551a029d6 Mon Sep 17 00:00:00 2001 From: Rostislav Melkumyan Date: Wed, 18 Oct 2023 10:47:49 +0200 Subject: [PATCH] feat: add draopTarget feature (#40) * feat: add placeholder (wip) * feat: put placeholder logic into a hook + docs + cleanup * feat: trailing new line * fix: types * fix: prop type * feat: format * feat: cleanup after review --- README.md | 3 +- src/{hooks.ts => hooks.tsx} | 65 +++++++++++++ src/index.tsx | 16 +++- stories/with-drop-target/index.stories.tsx | 104 +++++++++++++++++++++ 4 files changed, 185 insertions(+), 3 deletions(-) rename src/{hooks.ts => hooks.tsx} (84%) create mode 100644 stories/with-drop-target/index.stories.tsx diff --git a/README.md b/README.md index 3e845dd..03dde9b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ npm install react-easy-sort --save ```js import SortableList, { SortableItem } from 'react-easy-sort' -import { arrayMoveImmutable } from 'array-move'; +import { arrayMoveImmutable } from 'array-move' const App = () => { const [items, setItems] = React.useState(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']) @@ -80,6 +80,7 @@ const App = () => { | **lockAxis** | Determines if an axis should be locked | `'x'` or `'y'` | | | **allowDrag** | Determines whether items can be dragged | `boolean` | `true` | | **customHolderRef** | Ref of an element to use as a container for the dragged item | `React.RefObject` | `document.body` | +| **dropTarget** | React element to use as a dropTarget | `ReactNode` | | ### SortableItem diff --git a/src/hooks.ts b/src/hooks.tsx similarity index 84% rename from src/hooks.ts rename to src/hooks.tsx index bf5e067..1ab9f1a 100644 --- a/src/hooks.ts +++ b/src/hooks.tsx @@ -268,3 +268,68 @@ export const useDrag = ({ // https://developers.google.com/web/updates/2017/01/scrolling-intervention return isTouchDevice ? {} : { onMouseDown } } + +type UseDropTargetProps = Partial<{ + show: (sourceRect: DOMRect) => void + hide: () => void + setPosition: (index: number, itemsRect: DOMRect[], lockAxis?: 'x' | 'y') => void + render: () => React.ReactElement +}> + +export const useDropTarget = (content?: React.ReactNode): UseDropTargetProps => { + const dropTargetRef = React.useRef(null) + + if (!content) { + return {} + } + + const show = (sourceRect: DOMRect) => { + if (dropTargetRef.current) { + dropTargetRef.current.style.width = `${sourceRect.width}px` + dropTargetRef.current.style.height = `${sourceRect.height}px` + dropTargetRef.current.style.opacity = '1' + dropTargetRef.current.style.visibility = 'visible' + } + } + + const hide = () => { + if (dropTargetRef.current) { + dropTargetRef.current.style.opacity = '0' + dropTargetRef.current.style.visibility = 'hidden' + } + } + + const setPosition = (index: number, itemsRect: DOMRect[], lockAxis?: 'x' | 'y') => { + if (dropTargetRef.current) { + const sourceRect = itemsRect[index] + const newX = lockAxis === 'y' ? sourceRect.left : itemsRect[index].left + const newY = lockAxis === 'x' ? sourceRect.top : itemsRect[index].top + + dropTargetRef.current.style.transform = `translate3d(${newX}px, ${newY}px, 0px)` + } + } + + const DropTargetWrapper = (): React.ReactElement => ( +
+ {content} +
+ ) + + return { + show, + hide, + setPosition, + render: DropTargetWrapper, + } +} diff --git a/src/index.tsx b/src/index.tsx index 54c56ce..f298ac8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,7 @@ import arrayMove from 'array-move' import React, { HTMLAttributes } from 'react' import { findItemIndexAtPosition } from './helpers' -import { useDrag } from './hooks' +import { useDrag, useDropTarget } from './hooks' import { Point } from './types' const DEFAULT_CONTAINER_TAG = 'div' @@ -21,6 +21,8 @@ type Props = HTMLAttributes & { lockAxis?: 'x' | 'y' /** Reference to the Custom Holder element */ customHolderRef?: React.RefObject + /** Drop target to be used when dragging */ + dropTarget?: React.ReactNode } // this context is only used so that SortableItems can register/remove themselves @@ -41,6 +43,7 @@ const SortableList = ) => { // this array contains the elements than can be sorted (wrapped inside SortableItem) @@ -59,6 +62,8 @@ const SortableList = (undefined) // contains the offset point where the initial drag occurred to be used when dragging the item const offsetPointRef = React.useRef({ x: 0, y: 0 }) + // contains the dropTarget logic + const dropTargetLogic = useDropTarget(dropTarget) React.useEffect(() => { const holder = customHolderRef?.current || document.body @@ -157,6 +162,7 @@ const SortableList = { // we reset all items translations (the parent is expected to sort the items in the onSortEnd callback) @@ -245,6 +253,7 @@ const SortableList = {children} + + {children} + {dropTargetLogic.render?.()} + ) } diff --git a/stories/with-drop-target/index.stories.tsx b/stories/with-drop-target/index.stories.tsx new file mode 100644 index 0000000..265632f --- /dev/null +++ b/stories/with-drop-target/index.stories.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import arrayMove from 'array-move' + +import { action } from '@storybook/addon-actions' +import { Story } from '@storybook/react' + +import SortableList, { SortableItem } from '../../src/index' +import { generateItems } from '../helpers' +import { makeStyles } from '@material-ui/core' + +export default { + component: SortableList, + title: 'react-easy-sort/With drop target', + parameters: { + componentSubtitle: 'SortableList', + }, + argTypes: { + count: { + name: 'Number of elements', + control: { + type: 'range', + min: 3, + max: 12, + step: 1, + }, + defaultValue: 3, + }, + }, +} + +const useStyles = makeStyles({ + list: { + fontFamily: 'Helvetica, Arial, sans-serif', + userSelect: 'none', + display: 'grid', + gridTemplateColumns: 'auto auto auto', + gridGap: 16, + '@media (min-width: 600px)': { + gridGap: 24, + }, + }, + item: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgb(84, 84, 241)', + color: 'white', + height: 150, + cursor: 'grab', + fontSize: 20, + userSelect: 'none', + }, + dragged: { + backgroundColor: 'rgb(37, 37, 197)', + }, + dropTarget: { + border: '2px dashed rgb(84, 84, 241)', + height: 150, + boxSizing: 'border-box', + fontSize: 20, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: 'rgb(84, 84, 241)', + }, +}) + +type StoryProps = { + count: number +} + +export const Demo: Story = ({ count }: StoryProps) => { + const classes = useStyles() + + const [items, setItems] = React.useState([]) + React.useEffect(() => { + setItems(generateItems(count)) + }, [count]) + + const onSortEnd = (oldIndex: number, newIndex: number) => { + action('onSortEnd')(`oldIndex=${oldIndex}, newIndex=${newIndex}`) + setItems((array) => arrayMove(array, oldIndex, newIndex)) + } + + return ( + } + > + {items.map((item) => ( + +
{item}
+
+ ))} +
+ ) +} + +const DropTarget = () => { + const classes = useStyles() + return
Drop Target
+}