Skip to content

Commit ae68082

Browse files
authored
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
1 parent 8c8ad98 commit ae68082

File tree

4 files changed

+185
-3
lines changed

4 files changed

+185
-3
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ npm install react-easy-sort --save
4747

4848
```js
4949
import SortableList, { SortableItem } from 'react-easy-sort'
50-
import { arrayMoveImmutable } from 'array-move';
50+
import { arrayMoveImmutable } from 'array-move'
5151

5252
const App = () => {
5353
const [items, setItems] = React.useState(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'])
@@ -80,6 +80,7 @@ const App = () => {
8080
| **lockAxis** | Determines if an axis should be locked | `'x'` or `'y'` | |
8181
| **allowDrag** | Determines whether items can be dragged | `boolean` | `true` |
8282
| **customHolderRef** | Ref of an element to use as a container for the dragged item | `React.RefObject<HTMLElement \| null>` | `document.body` |
83+
| **dropTarget** | React element to use as a dropTarget | `ReactNode` | |
8384

8485
### SortableItem
8586

src/hooks.ts renamed to src/hooks.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,68 @@ export const useDrag = ({
268268
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
269269
return isTouchDevice ? {} : { onMouseDown }
270270
}
271+
272+
type UseDropTargetProps = Partial<{
273+
show: (sourceRect: DOMRect) => void
274+
hide: () => void
275+
setPosition: (index: number, itemsRect: DOMRect[], lockAxis?: 'x' | 'y') => void
276+
render: () => React.ReactElement
277+
}>
278+
279+
export const useDropTarget = (content?: React.ReactNode): UseDropTargetProps => {
280+
const dropTargetRef = React.useRef<HTMLDivElement | null>(null)
281+
282+
if (!content) {
283+
return {}
284+
}
285+
286+
const show = (sourceRect: DOMRect) => {
287+
if (dropTargetRef.current) {
288+
dropTargetRef.current.style.width = `${sourceRect.width}px`
289+
dropTargetRef.current.style.height = `${sourceRect.height}px`
290+
dropTargetRef.current.style.opacity = '1'
291+
dropTargetRef.current.style.visibility = 'visible'
292+
}
293+
}
294+
295+
const hide = () => {
296+
if (dropTargetRef.current) {
297+
dropTargetRef.current.style.opacity = '0'
298+
dropTargetRef.current.style.visibility = 'hidden'
299+
}
300+
}
301+
302+
const setPosition = (index: number, itemsRect: DOMRect[], lockAxis?: 'x' | 'y') => {
303+
if (dropTargetRef.current) {
304+
const sourceRect = itemsRect[index]
305+
const newX = lockAxis === 'y' ? sourceRect.left : itemsRect[index].left
306+
const newY = lockAxis === 'x' ? sourceRect.top : itemsRect[index].top
307+
308+
dropTargetRef.current.style.transform = `translate3d(${newX}px, ${newY}px, 0px)`
309+
}
310+
}
311+
312+
const DropTargetWrapper = (): React.ReactElement => (
313+
<div
314+
ref={dropTargetRef}
315+
aria-hidden
316+
style={{
317+
opacity: 0,
318+
visibility: 'hidden',
319+
position: 'fixed',
320+
top: 0,
321+
left: 0,
322+
pointerEvents: 'none',
323+
}}
324+
>
325+
{content}
326+
</div>
327+
)
328+
329+
return {
330+
show,
331+
hide,
332+
setPosition,
333+
render: DropTargetWrapper,
334+
}
335+
}

src/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import arrayMove from 'array-move'
22
import React, { HTMLAttributes } from 'react'
33

44
import { findItemIndexAtPosition } from './helpers'
5-
import { useDrag } from './hooks'
5+
import { useDrag, useDropTarget } from './hooks'
66
import { Point } from './types'
77

88
const DEFAULT_CONTAINER_TAG = 'div'
@@ -21,6 +21,8 @@ type Props<TTag extends keyof JSX.IntrinsicElements> = HTMLAttributes<TTag> & {
2121
lockAxis?: 'x' | 'y'
2222
/** Reference to the Custom Holder element */
2323
customHolderRef?: React.RefObject<HTMLElement | null>
24+
/** Drop target to be used when dragging */
25+
dropTarget?: React.ReactNode
2426
}
2527

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

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

159164
updateTargetPosition(pointInWindow)
165+
dropTargetLogic.show?.(sourceRect)
160166

161167
// Adds a nice little physical feedback
162168
if (window.navigator.vibrate) {
@@ -215,6 +221,8 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
215221
// we want the translation to be animated
216222
currentItem.style.transitionDuration = '300ms'
217223
}
224+
225+
dropTargetLogic.setPosition?.(lastTargetIndexRef.current, itemsRect.current, lockAxis)
218226
},
219227
onEnd: () => {
220228
// we reset all items translations (the parent is expected to sort the items in the onSortEnd callback)
@@ -245,6 +253,7 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
245253
}
246254
sourceIndexRef.current = undefined
247255
lastTargetIndexRef.current = undefined
256+
dropTargetLogic.hide?.()
248257

249258
// cleanup the target element from the DOM
250259
if (targetRef.current) {
@@ -294,7 +303,10 @@ const SortableList = <TTag extends keyof JSX.IntrinsicElements = typeof DEFAULT_
294303
...rest,
295304
ref: containerRef,
296305
},
297-
<SortableListContext.Provider value={context}>{children}</SortableListContext.Provider>
306+
<SortableListContext.Provider value={context}>
307+
{children}
308+
{dropTargetLogic.render?.()}
309+
</SortableListContext.Provider>
298310
)
299311
}
300312

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from 'react'
2+
import arrayMove from 'array-move'
3+
4+
import { action } from '@storybook/addon-actions'
5+
import { Story } from '@storybook/react'
6+
7+
import SortableList, { SortableItem } from '../../src/index'
8+
import { generateItems } from '../helpers'
9+
import { makeStyles } from '@material-ui/core'
10+
11+
export default {
12+
component: SortableList,
13+
title: 'react-easy-sort/With drop target',
14+
parameters: {
15+
componentSubtitle: 'SortableList',
16+
},
17+
argTypes: {
18+
count: {
19+
name: 'Number of elements',
20+
control: {
21+
type: 'range',
22+
min: 3,
23+
max: 12,
24+
step: 1,
25+
},
26+
defaultValue: 3,
27+
},
28+
},
29+
}
30+
31+
const useStyles = makeStyles({
32+
list: {
33+
fontFamily: 'Helvetica, Arial, sans-serif',
34+
userSelect: 'none',
35+
display: 'grid',
36+
gridTemplateColumns: 'auto auto auto',
37+
gridGap: 16,
38+
'@media (min-width: 600px)': {
39+
gridGap: 24,
40+
},
41+
},
42+
item: {
43+
display: 'flex',
44+
justifyContent: 'center',
45+
alignItems: 'center',
46+
backgroundColor: 'rgb(84, 84, 241)',
47+
color: 'white',
48+
height: 150,
49+
cursor: 'grab',
50+
fontSize: 20,
51+
userSelect: 'none',
52+
},
53+
dragged: {
54+
backgroundColor: 'rgb(37, 37, 197)',
55+
},
56+
dropTarget: {
57+
border: '2px dashed rgb(84, 84, 241)',
58+
height: 150,
59+
boxSizing: 'border-box',
60+
fontSize: 20,
61+
display: 'flex',
62+
justifyContent: 'center',
63+
alignItems: 'center',
64+
color: 'rgb(84, 84, 241)',
65+
},
66+
})
67+
68+
type StoryProps = {
69+
count: number
70+
}
71+
72+
export const Demo: Story<StoryProps> = ({ count }: StoryProps) => {
73+
const classes = useStyles()
74+
75+
const [items, setItems] = React.useState<string[]>([])
76+
React.useEffect(() => {
77+
setItems(generateItems(count))
78+
}, [count])
79+
80+
const onSortEnd = (oldIndex: number, newIndex: number) => {
81+
action('onSortEnd')(`oldIndex=${oldIndex}, newIndex=${newIndex}`)
82+
setItems((array) => arrayMove(array, oldIndex, newIndex))
83+
}
84+
85+
return (
86+
<SortableList
87+
onSortEnd={onSortEnd}
88+
className={classes.list}
89+
draggedItemClassName={classes.dragged}
90+
dropTarget={<DropTarget />}
91+
>
92+
{items.map((item) => (
93+
<SortableItem key={item}>
94+
<div className={classes.item}>{item}</div>
95+
</SortableItem>
96+
))}
97+
</SortableList>
98+
)
99+
}
100+
101+
const DropTarget = () => {
102+
const classes = useStyles()
103+
return <div className={classes.dropTarget}>Drop Target</div>
104+
}

0 commit comments

Comments
 (0)