Skip to content

Commit

Permalink
feat: add draopTarget feature (#40)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
RostiMelk authored Oct 18, 2023
1 parent 8c8ad98 commit ae68082
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down Expand Up @@ -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<HTMLElement \| null>` | `document.body` |
| **dropTarget** | React element to use as a dropTarget | `ReactNode` | |

### SortableItem

Expand Down
65 changes: 65 additions & 0 deletions src/hooks.ts → src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement | null>(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 => (
<div
ref={dropTargetRef}
aria-hidden
style={{
opacity: 0,
visibility: 'hidden',
position: 'fixed',
top: 0,
left: 0,
pointerEvents: 'none',
}}
>
{content}
</div>
)

return {
show,
hide,
setPosition,
render: DropTargetWrapper,
}
}
16 changes: 14 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,6 +21,8 @@ type Props<TTag extends keyof JSX.IntrinsicElements> = HTMLAttributes<TTag> & {
lockAxis?: 'x' | 'y'
/** Reference to the Custom Holder element */
customHolderRef?: React.RefObject<HTMLElement | null>
/** Drop target to be used when dragging */
dropTarget?: React.ReactNode
}

// this context is only used so that SortableItems can register/remove themselves
Expand All @@ -41,6 +43,7 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
as,
lockAxis,
customHolderRef,
dropTarget,
...rest
}: Props<TTag>) => {
// this array contains the elements than can be sorted (wrapped inside SortableItem)
Expand All @@ -59,6 +62,8 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
const lastTargetIndexRef = React.useRef<number | undefined>(undefined)
// contains the offset point where the initial drag occurred to be used when dragging the item
const offsetPointRef = React.useRef<Point>({ x: 0, y: 0 })
// contains the dropTarget logic
const dropTargetLogic = useDropTarget(dropTarget)

React.useEffect(() => {
const holder = customHolderRef?.current || document.body
Expand Down Expand Up @@ -157,6 +162,7 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
}

updateTargetPosition(pointInWindow)
dropTargetLogic.show?.(sourceRect)

// Adds a nice little physical feedback
if (window.navigator.vibrate) {
Expand Down Expand Up @@ -215,6 +221,8 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
// we want the translation to be animated
currentItem.style.transitionDuration = '300ms'
}

dropTargetLogic.setPosition?.(lastTargetIndexRef.current, itemsRect.current, lockAxis)
},
onEnd: () => {
// we reset all items translations (the parent is expected to sort the items in the onSortEnd callback)
Expand Down Expand Up @@ -245,6 +253,7 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
}
sourceIndexRef.current = undefined
lastTargetIndexRef.current = undefined
dropTargetLogic.hide?.()

// cleanup the target element from the DOM
if (targetRef.current) {
Expand Down Expand Up @@ -294,7 +303,10 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
...rest,
ref: containerRef,
},
<SortableListContext.Provider value={context}>{children}</SortableListContext.Provider>
<SortableListContext.Provider value={context}>
{children}
{dropTargetLogic.render?.()}
</SortableListContext.Provider>
)
}

Expand Down
104 changes: 104 additions & 0 deletions stories/with-drop-target/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<StoryProps> = ({ count }: StoryProps) => {
const classes = useStyles()

const [items, setItems] = React.useState<string[]>([])
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 (
<SortableList
onSortEnd={onSortEnd}
className={classes.list}
draggedItemClassName={classes.dragged}
dropTarget={<DropTarget />}
>
{items.map((item) => (
<SortableItem key={item}>
<div className={classes.item}>{item}</div>
</SortableItem>
))}
</SortableList>
)
}

const DropTarget = () => {
const classes = useStyles()
return <div className={classes.dropTarget}>Drop Target</div>
}

0 comments on commit ae68082

Please sign in to comment.