Skip to content

Commit 1195df8

Browse files
authored
feat: Animate drag cancellation and inside drop back (#16)
1 parent ef0238a commit 1195df8

File tree

6 files changed

+92
-30
lines changed

6 files changed

+92
-30
lines changed

README.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,16 @@ yarn add @untemps/svelte-use-drop-outside
100100

101101
## API
102102

103-
| Props | Type | Default | Description |
104-
|-----------------------|-----------------------------|--------------------|-------------------------------------------------------------------------|
105-
| `areaSelector` | string | null | Selector of the element considered as the "inside" area. |
106-
| `dragImage` | element or object or string | null | The image used when the element is dragging. |
107-
| `dragClassName` | string | null | A class name that will be assigned to the dragged element. |
108-
| `onDropOutside` | function | null | Callback triggered when the dragged element is dropped outside the area. |
109-
| `onDropInside` | function | null | Callback triggered when the dragged element is dropped inside the area |
110-
| `onDragCancel` | function | null | Callback triggered when the drag is cancelled (Esc key) |
103+
| Props | Type | Default | Description |
104+
|------------------|-----------------------------|------------------------------------------|----------------------------------------------------------------------------------------|
105+
| `areaSelector` | string | null | Selector of the element considered as the "inside" area. |
106+
| `dragImage` | element or object or string | null | The image used when the element is dragging. |
107+
| `dragClassName` | string | null | A class name that will be assigned to the dragged element. |
108+
| `animate` | boolean | false | A flag to enable animation back. |
109+
| `animateOptions` | object | { duration: .2, timingFunction: 'ease' } | Optional options for the animation back (see [Animation Options](#animation-options)). |
110+
| `onDropOutside` | function | null | Callback triggered when the dragged element is dropped outside the area. |
111+
| `onDropInside` | function | null | Callback triggered when the dragged element is dropped inside the area |
112+
| `onDragCancel` | function | null | Callback triggered when the drag is cancelled (Esc key) |
111113

112114
### Area Selector
113115

@@ -237,13 +239,28 @@ The class declaration will be parsed and set to the `style` attribute of the dra
237239
</style>
238240
```
239241

242+
### Animation
243+
244+
By default, when the dragged element is dropped inside the area or if the drag is cancelled, the dragged element is plainly removed.
245+
246+
When setting the `animate` to `true`, when those events happen, the dragged element is smoothly moved back to its original position.
247+
248+
#### Animation Options
249+
250+
The animation can be configured through the `animateOptions` prop:
251+
252+
| Argument | Type | Default | Description |
253+
|----------------|--------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
254+
| duration | number | .2 | Duration of the animation (in seconds). |
255+
| timingFunction | string | 'ease' | Function that defines the animation effect (see [animation-timing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function)). |
256+
240257
### Callbacks
241258

242259
All callbacks are triggered with the following arguments:
243260

244-
| Argument | Description |
245-
|----------|-------------------------------------------|
246-
| [0] | Dragged element. |
261+
| Argument | Description |
262+
|----------|------------------------------------------|
263+
| [0] | Dragged element. |
247264
| [1] | Element considered as the "inside" area. |
248265

249266
```javascript

dev/src/App.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
{#each colors as color, index}
4545
<li use:useDropOutside={{
4646
areaSelector: '.area',
47+
animate: true,
48+
animateOptions: {
49+
timingFunction: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
50+
},
4751
onDropOutside: _onDropOutside,
4852
onDropInside: _onDropInside,
4953
onDragCancel: _onDragCancel,

src/DragAndDrop.js

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ class DragAndDrop {
1111

1212
#target = null
1313
#dragImage = null
14+
#animate = null
1415
#dragClassName = null
16+
#animateOptions = null
1517
#onDropOutside = null
1618
#onDropInside = null
1719
#onDragCancel = null
@@ -37,10 +39,22 @@ class DragAndDrop {
3739
DragAndDrop.instances = []
3840
}
3941

40-
constructor(target, areaSelector, dragImage, dragClassName, onDropOutside, onDropInside, onDragCancel) {
42+
constructor(
43+
target,
44+
areaSelector,
45+
dragImage,
46+
dragClassName,
47+
animate,
48+
animateOptions,
49+
onDropOutside,
50+
onDropInside,
51+
onDragCancel
52+
) {
4153
this.#target = target
4254
this.#dragImage = dragImage
4355
this.#dragClassName = dragClassName
56+
this.#animate = animate || false
57+
this.#animateOptions = { duration: 0.2, timingFunction: 'ease', ...(animateOptions || {}) }
4458
this.#onDropOutside = onDropOutside
4559
this.#onDropInside = onDropInside
4660
this.#onDragCancel = onDragCancel
@@ -92,6 +106,26 @@ class DragAndDrop {
92106
this.#observer = null
93107
}
94108

109+
#animateBack(callback) {
110+
if (this.#animate) {
111+
this.#drag.style.setProperty('--origin-x', this.#target.getBoundingClientRect().left + 'px')
112+
this.#drag.style.setProperty('--origin-y', this.#target.getBoundingClientRect().top + 'px')
113+
this.#drag.style.animation = `move ${this.#animateOptions.duration}s ${this.#animateOptions.timingFunction}`
114+
this.#drag.addEventListener(
115+
'animationend',
116+
() => {
117+
this.#drag.style.animation = 'none'
118+
this.#drag.remove()
119+
callback?.(this.#target, this.#area)
120+
},
121+
false
122+
)
123+
} else {
124+
this.#drag.remove()
125+
callback?.(this.#target, this.#area)
126+
}
127+
}
128+
95129
#onMouseOver(e) {
96130
e.target.style.cursor = 'grab'
97131
}
@@ -151,17 +185,14 @@ class DragAndDrop {
151185

152186
const doOverlap = doElementsOverlap(this.#area, this.#drag)
153187

154-
this.#drag.remove()
155-
156-
setTimeout(() => {
157-
if (e.type.startsWith('key')) {
158-
this.#onDragCancel?.(this.#target, this.#area)
159-
} else if (doOverlap) {
160-
this.#onDropInside?.(this.#target, this.#area)
161-
} else {
162-
this.#onDropOutside?.(this.#target, this.#area)
163-
}
164-
}, 10)
188+
if (e.type.startsWith('key')) {
189+
this.#animateBack(this.#onDragCancel)
190+
} else if (doOverlap) {
191+
this.#animateBack(this.#onDropInside)
192+
} else {
193+
this.#drag.remove()
194+
this.#onDropOutside?.(this.#target, this.#area)
195+
}
165196
}
166197
}
167198

src/__tests__/useDropOutside.test.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { fireEvent, screen } from '@testing-library/dom'
66

77
import { createElement } from '@untemps/utils/dom/createElement'
88
import { getElement } from '@untemps/utils/dom/getElement'
9-
import { standby } from '@untemps/utils/async/standby'
109

1110
import DragAndDrop from '../DragAndDrop'
1211
import useDropOutside from '../useDropOutside'
@@ -119,7 +118,7 @@ describe('useDropOutside', () => {
119118
})
120119

121120
it('Triggers onDropInside callback', async () => {
122-
useReturn = useDropOutside(target, options)
121+
useReturn = useDropOutside(target, { ...options, animate: true })
123122
fireEvent.touchStart(target, { targetTouches: [{ pageX: 10, pageY: 10 }] })
124123
fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] })
125124
fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] }) // Duplicate on purpose
@@ -133,7 +132,7 @@ describe('useDropOutside', () => {
133132
bottom: targetSize,
134133
})
135134
fireEvent.mouseUp(document)
136-
await standby()
135+
fireEvent.animationEnd(screen.getByRole('presentation'))
137136
expect(onDropInside).toHaveBeenCalled()
138137
})
139138

@@ -151,17 +150,16 @@ describe('useDropOutside', () => {
151150
bottom: areaSize + 10 + targetSize,
152151
})
153152
fireEvent.mouseUp(document)
154-
await standby()
155153
expect(onDropOutside).toHaveBeenCalled()
156154
})
157155

158156
it('Triggers onDragCancel callback', async () => {
159-
useReturn = useDropOutside(target, options)
157+
useReturn = useDropOutside(target, { ...options, animate: true })
160158
fireEvent.mouseDown(target)
161159
fireEvent.mouseMove(document)
162160
fireEvent.keyDown(document, { key: 'A', code: 'A' })
163161
fireEvent.keyDown(document, { key: 'Escape', code: 'Esc' })
164-
await standby()
162+
fireEvent.animationEnd(screen.getByRole('presentation'))
165163
expect(onDragCancel).toHaveBeenCalled()
166164
})
167165

src/useDropOutside.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,14 @@
33
z-index: 999;
44
user-select: none;
55
opacity: .7;
6+
7+
--origin-x: 0px;
8+
--origin-y: 0px;
9+
}
10+
11+
@keyframes move {
12+
100% {
13+
left: var(--origin-x);
14+
top: var(--origin-y);
15+
}
616
}

src/useDropOutside.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import DragAndDrop from './DragAndDrop'
22

33
const useDropOutside = (
44
node,
5-
{ areaSelector, dragImage, dragClassName, onDropOutside, onDropInside, onDragCancel }
5+
{ areaSelector, dragImage, dragClassName, animate, animateOptions, onDropOutside, onDropInside, onDragCancel }
66
) => {
77
const instance = new DragAndDrop(
88
node,
99
areaSelector,
1010
dragImage,
1111
dragClassName,
12+
animate,
13+
animateOptions,
1214
onDropOutside,
1315
onDropInside,
1416
onDragCancel

0 commit comments

Comments
 (0)