Skip to content

Commit ec958d9

Browse files
committed
feat: implement handling of drag events (file drag & drop)
Apart from the usual canvas dragenter & dragleave, this commits adds special handlers: onDragOverEnter, onDragOverLeave & onDragOverMissed. These are fired when dragover events intersect with objects in a scene or miss all of them, similar to how onPointerMissed already works. onDrop and onDropMissed are other additions. These can come handy when working on editor UIs etc. that need to attribute different drag & drop actions to different objects.
1 parent 0921539 commit ec958d9

File tree

10 files changed

+303
-9
lines changed

10 files changed

+303
-9
lines changed

docs/API/events.mdx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ nav: 8
88

99
Additionally, there's a special `onUpdate` that is called every time the object gets fresh props, which is good for things like `self => (self.verticesNeedUpdate = true)`.
1010

11-
Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes.
11+
Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes. Similarly, `onDragOverMissed` and `onDropMissed` can handle actions that need to be taken when file drag & drop drag events are not hitting items in the scene.
1212

1313
```jsx
1414
<mesh
1515
onClick={(e) => console.log('click')}
1616
onContextMenu={(e) => console.log('context menu')}
1717
onDoubleClick={(e) => console.log('double click')}
18+
onDragEnter={(e) => console.log('drag enter')}
19+
onDragLeave={(e) => console.log('drag leave')}
20+
onDragOverEnter={(e) => console.log('dragover enter')}
21+
onDragOverLeave={(e) => console.log('dragover leave')}
22+
onDragOverMissed={(e) => console.log('dragover missed')}
23+
onDrop={(e) => console.log('dropped')}
24+
onDropMissed={(e) => console.log('drop missed')}
1825
onWheel={(e) => console.log('wheel spins')}
1926
onPointerUp={(e) => console.log('up')}
2027
onPointerDown={(e) => console.log('down')}

example/src/demos/FileDragDrop.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { SyntheticEvent, useState } from 'react'
2+
import { Canvas } from '@react-three/fiber'
3+
import { a, useSpring } from '@react-spring/three'
4+
import { OrbitControls } from '@react-three/drei'
5+
6+
export default function Box() {
7+
const [active, setActive] = useState(0)
8+
const [activeBg, setActiveBg] = useState(0)
9+
// create a common spring that will be used later to interpolate other values
10+
const { spring } = useSpring({
11+
spring: active,
12+
config: { mass: 5, tension: 400, friction: 50, precision: 0.0001 },
13+
})
14+
// interpolate values from commong spring
15+
const scale = spring.to([0, 1], [1, 2])
16+
const rotation = spring.to([0, 1], [0, Math.PI])
17+
const color = active ? spring.to([0, 1], ['#6246ea', '#e45858']) : spring.to([0, 1], ['#620000', '#e40000'])
18+
const bgColor = activeBg ? 'lightgreen' : 'lightgray'
19+
const preventDragDropDefaults = {
20+
onDrop: (e: SyntheticEvent) => e.preventDefault(),
21+
onDragEnter: (e: SyntheticEvent) => e.preventDefault(),
22+
onDragOver: (e: SyntheticEvent) => e.preventDefault(),
23+
}
24+
return (
25+
<Canvas
26+
{...preventDragDropDefaults}
27+
onDropMissed={(e) => {
28+
console.log('drop missed!')
29+
setActiveBg(0)
30+
}}
31+
onDragOverMissed={(e) => setActiveBg(1)}
32+
onDragLeave={() => setActiveBg(0)}>
33+
<color attach="background" args={[bgColor]} />
34+
<a.mesh
35+
rotation-y={rotation}
36+
scale-x={scale}
37+
scale-z={scale}
38+
onDrop={(e) => {
39+
console.log('dropped!')
40+
setActive(0)
41+
}}
42+
onDragOverEnter={() => {
43+
setActive(1)
44+
setActiveBg(0)
45+
}}
46+
onDragOverLeave={() => setActive(0)}>
47+
<boxBufferGeometry />
48+
<a.meshBasicMaterial color={color} />
49+
</a.mesh>
50+
<OrbitControls />
51+
</Canvas>
52+
)
53+
}

example/src/demos/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Animation = { Component: lazy(() => import('./Animation')) }
44
const AutoDispose = { Component: lazy(() => import('./AutoDispose')) }
55
const ClickAndHover = { Component: lazy(() => import('./ClickAndHover')) }
66
const ContextMenuOverride = { Component: lazy(() => import('./ContextMenuOverride')) }
7+
const FileDragDrop = { Component: lazy(() => import('./FileDragDrop')) }
78
const Gestures = { Component: lazy(() => import('./Gestures')) }
89
const Gltf = { Component: lazy(() => import('./Gltf')) }
910
const Inject = { Component: lazy(() => import('./Inject')) }
@@ -30,6 +31,7 @@ export {
3031
AutoDispose,
3132
ClickAndHover,
3233
ContextMenuOverride,
34+
FileDragDrop,
3335
Gestures,
3436
Gltf,
3537
Inject,

packages/fiber/src/core/events.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export type Events = {
4141
onClick: EventListener
4242
onContextMenu: EventListener
4343
onDoubleClick: EventListener
44+
onDragEnter: EventListener
45+
onDragLeave: EventListener
46+
onDragOverEnter: EventListener
47+
onDragOverLeave: EventListener
48+
onDrop: EventListener
49+
onDropMissed: EventListener
4450
onWheel: EventListener
4551
onPointerDown: EventListener
4652
onPointerUp: EventListener
@@ -54,6 +60,13 @@ export type EventHandlers = {
5460
onClick?: (event: ThreeEvent<MouseEvent>) => void
5561
onContextMenu?: (event: ThreeEvent<MouseEvent>) => void
5662
onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void
63+
onDragEnter?: (event: ThreeEvent<DragEvent>) => void
64+
onDragLeave?: (event: ThreeEvent<DragEvent>) => void
65+
onDragOverEnter?: (event: ThreeEvent<DragEvent>) => void
66+
onDragOverLeave?: (event: ThreeEvent<DragEvent>) => void
67+
onDragOverMissed?: (event: DragEvent) => void
68+
onDrop?: (event: ThreeEvent<DragEvent>) => void
69+
onDropMissed?: (event: DragEvent) => void
5770
onPointerUp?: (event: ThreeEvent<PointerEvent>) => void
5871
onPointerDown?: (event: ThreeEvent<PointerEvent>) => void
5972
onPointerOver?: (event: ThreeEvent<PointerEvent>) => void
@@ -105,10 +118,14 @@ export function getEventPriority() {
105118
case 'click':
106119
case 'contextmenu':
107120
case 'dblclick':
121+
case 'dragenter':
122+
case 'dragleave':
123+
case 'drop':
108124
case 'pointercancel':
109125
case 'pointerdown':
110126
case 'pointerup':
111127
return DiscreteEventPriority
128+
case 'dragover':
112129
case 'pointermove':
113130
case 'pointerout':
114131
case 'pointerover':
@@ -171,10 +188,14 @@ export function createEvents(store: UseBoundStore<RootState>) {
171188

172189
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
173190
function filterPointerEvents(objects: THREE.Object3D[]) {
174-
return objects.filter((obj) =>
175-
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
176-
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
177-
),
191+
return objects.filter(
192+
(obj) =>
193+
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
194+
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
195+
) ||
196+
['Over', 'Enter', 'Leave'].some(
197+
(name) => (obj as unknown as Instance).__r3f?.handlers[('onDrag' + name) as keyof EventHandlers],
198+
),
178199
)
179200
}
180201

@@ -377,6 +398,8 @@ export function createEvents(store: UseBoundStore<RootState>) {
377398
const data = { ...hoveredObj, intersections }
378399
handlers.onPointerOut?.(data as ThreeEvent<PointerEvent>)
379400
handlers.onPointerLeave?.(data as ThreeEvent<PointerEvent>)
401+
// @ts-ignore
402+
handlers.onDragOverLeave?.(data)
380403
}
381404
}
382405
})
@@ -387,6 +410,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
387410
switch (name) {
388411
case 'onPointerLeave':
389412
case 'onPointerCancel':
413+
case 'onDragLeave':
390414
return () => cancelPointer([])
391415
case 'onLostPointerCapture':
392416
return (event: DomEvent) => {
@@ -402,13 +426,15 @@ export function createEvents(store: UseBoundStore<RootState>) {
402426

403427
// Any other pointer goes here ...
404428
return (event: DomEvent) => {
405-
const { onPointerMissed, internal } = store.getState()
429+
const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = store.getState()
406430

407431
//prepareRay(event)
408432
internal.lastEvent.current = event
409433

410434
// Get fresh intersects
411435
const isPointerMove = name === 'onPointerMove'
436+
const isDragOver = name === 'onDragOver'
437+
const isDrop = name === 'onDrop'
412438
const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick'
413439
const filter = isPointerMove ? filterPointerEvents : undefined
414440
//const hits = patchIntersects(intersect(filter), event)
@@ -429,8 +455,17 @@ export function createEvents(store: UseBoundStore<RootState>) {
429455
if (onPointerMissed) onPointerMissed(event)
430456
}
431457
}
458+
if (isDragOver && !hits.length) {
459+
dragOverMissed(event as DragEvent, internal.interaction)
460+
if (onDragOverMissed) onDragOverMissed(event as DragEvent)
461+
}
462+
if (isDrop && !hits.length) {
463+
dropMissed(event as DragEvent, internal.interaction)
464+
if (onDropMissed) onDropMissed(event as DragEvent)
465+
}
466+
432467
// Take care of unhover
433-
if (isPointerMove) cancelPointer(hits)
468+
if (isPointerMove || isDragOver) cancelPointer(hits)
434469

435470
handleIntersects(hits, event, delta, (data: ThreeEvent<DomEvent>) => {
436471
const eventObject = data.eventObject
@@ -457,6 +492,23 @@ export function createEvents(store: UseBoundStore<RootState>) {
457492
}
458493
// Call mouse move
459494
handlers.onPointerMove?.(data as ThreeEvent<PointerEvent>)
495+
} else if (isDragOver) {
496+
// When enter or out is present take care of hover-state
497+
const id = makeId(data)
498+
const hoveredItem = internal.hovered.get(id)
499+
if (!hoveredItem) {
500+
// If the object wasn't previously hovered, book it and call its handler
501+
internal.hovered.set(id, data)
502+
handlers.onDragOverEnter?.(data as ThreeEvent<DragEvent>)
503+
} else if (hoveredItem.stopped) {
504+
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
505+
data.stopPropagation()
506+
} else if (internal.initialHits.includes(eventObject)) {
507+
dragOverMissed(
508+
event as DragEvent,
509+
internal.interaction.filter((object) => !internal.initialHits.includes(object)),
510+
)
511+
}
460512
} else {
461513
// All other events ...
462514
const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent<PointerEvent>) => void
@@ -492,5 +544,15 @@ export function createEvents(store: UseBoundStore<RootState>) {
492544
)
493545
}
494546

547+
function dragOverMissed(event: DragEvent, objects: THREE.Object3D[]) {
548+
objects.forEach((object: THREE.Object3D) =>
549+
(object as unknown as Instance).__r3f?.handlers.onDragOverMissed?.(event),
550+
)
551+
}
552+
553+
function dropMissed(event: DragEvent, objects: THREE.Object3D[]) {
554+
objects.forEach((object: THREE.Object3D) => (object as unknown as Instance).__r3f?.handlers.onDropMissed?.(event))
555+
}
556+
495557
return { handlePointer }
496558
}

packages/fiber/src/core/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ export type RenderProps<TCanvas extends Element> = {
9898
onCreated?: (state: RootState) => void
9999
/** Response for pointer clicks that have missed any target */
100100
onPointerMissed?: (event: MouseEvent) => void
101+
/** Response for dragover events that have missed any target */
102+
onDragOverMissed?: (event: DragEvent) => void
103+
/** Response for drop events that have missed any target */
104+
onDropMissed?: (event: DragEvent) => void
101105
}
102106

103107
const createRendererInstance = <TElement extends Element>(gl: GLProps, canvas: TElement): THREE.WebGLRenderer => {
@@ -169,6 +173,8 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
169173
raycaster: raycastOptions,
170174
camera: cameraOptions,
171175
onPointerMissed,
176+
onDragOverMissed,
177+
onDropMissed,
172178
} = props
173179

174180
let state = store.getState()
@@ -282,6 +288,10 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
282288
if (state.frameloop !== frameloop) state.setFrameloop(frameloop)
283289
// Check pointer missed
284290
if (!state.onPointerMissed) state.set({ onPointerMissed })
291+
// Check dragover missed
292+
if (!state.onDragOverMissed) state.set({ onDragOverMissed })
293+
// Check drop missed
294+
if (!state.onDropMissed) state.set({ onDropMissed })
285295
// Check performance
286296
if (performance && !is.equ(performance, state.performance, shallowLoose))
287297
state.set((state) => ({ performance: { ...state.performance, ...performance } }))

packages/fiber/src/core/store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ export type RootState = {
141141
setFrameloop: (frameloop?: 'always' | 'demand' | 'never') => void
142142
/** When the canvas was clicked but nothing was hit */
143143
onPointerMissed?: (event: MouseEvent) => void
144+
/** When the canvas was dragover but nothing was hit */
145+
onDragOverMissed?: (event: DragEvent) => void
146+
/** When the canvas was dropped but nothing was hit */
147+
onDropMissed?: (event: DragEvent) => void
144148
/** If this state model is layerd (via createPortal) then this contains the previous layer */
145149
previousRoot?: UseBoundStore<RootState, StoreApi<RootState>>
146150
/** Internals */
@@ -209,6 +213,8 @@ const createStore = (
209213

210214
frameloop: 'always',
211215
onPointerMissed: undefined,
216+
onDragOverMissed: undefined,
217+
onDropMissed: undefined,
212218

213219
performance: {
214220
current: 1,

packages/fiber/src/core/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ export function diffProps(
219219
// When props match bail out
220220
if (is.equ(value, previous[key])) return
221221
// Collect handlers and bail out
222-
if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) return changes.push([key, value, true, []])
222+
if (/^on(Pointer|DragOver|Drop|Click|DoubleClick|ContextMenu|Wheel)/.test(key))
223+
return changes.push([key, value, true, []])
223224
// Split dashed props
224225
let entries: string[] = []
225226
if (key.includes('-')) entries = key.split('-')

packages/fiber/src/web/Canvas.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
4040
raycaster,
4141
camera,
4242
onPointerMissed,
43+
onDragOverMissed,
44+
onDropMissed,
4345
onCreated,
4446
...props
4547
},
@@ -57,6 +59,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
5759
React.useImperativeHandle(forwardedRef, () => canvasRef.current)
5860

5961
const handlePointerMissed = useMutableCallback(onPointerMissed)
62+
const handleDragOverMissed = useMutableCallback(onDragOverMissed)
63+
const handleDropMissed = useMutableCallback(onDropMissed)
6064
const [block, setBlock] = React.useState<SetBlock>(false)
6165
const [error, setError] = React.useState<any>(false)
6266

@@ -85,6 +89,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
8589
size: { width, height },
8690
// Pass mutable reference to onPointerMissed so it's free to update
8791
onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
92+
onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
93+
onDropMissed: (...args) => handleDropMissed.current?.(...args),
8894
onCreated: (state) => {
8995
state.events.connect?.(divRef.current)
9096
onCreated?.(state)

packages/fiber/src/web/events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const DOM_EVENTS = {
66
onClick: ['click', false],
77
onContextMenu: ['contextmenu', false],
88
onDoubleClick: ['dblclick', false],
9+
onDragEnter: ['dragenter', false],
10+
onDragLeave: ['dragleave', false],
11+
onDragOver: ['dragover', false],
12+
onDrop: ['drop', false],
913
onWheel: ['wheel', true],
1014
onPointerDown: ['pointerdown', true],
1115
onPointerUp: ['pointerup', true],

0 commit comments

Comments
 (0)