Skip to content

Commit f89dab3

Browse files
committed
Implement onDragOverEnter & onDragOverLeave
1 parent cdbeaf9 commit f89dab3

File tree

5 files changed

+72
-7
lines changed

5 files changed

+72
-7
lines changed

docs/API/events.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Also notice the `onPointerMissed` on the canvas element, which fires on clicks t
1515
onClick={(e) => console.log('click')}
1616
onContextMenu={(e) => console.log('context menu')}
1717
onDoubleClick={(e) => console.log('double click')}
18+
onDragOverEnter={(e) => console.log('dragover enter')}
19+
onDragOverLeave={(e) => console.log('dragover leave')}
1820
onWheel={(e) => console.log('wheel spins')}
1921
onPointerUp={(e) => console.log('up')}
2022
onPointerDown={(e) => console.log('down')}

packages/fiber/src/core/events.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export type Events = {
4141
onClick: EventListener
4242
onContextMenu: EventListener
4343
onDoubleClick: EventListener
44+
onDragOverEnter: EventListener
45+
onDragOverLeave: EventListener
4446
onWheel: EventListener
4547
onPointerDown: EventListener
4648
onPointerUp: EventListener
@@ -54,6 +56,8 @@ export type EventHandlers = {
5456
onClick?: (event: ThreeEvent<MouseEvent>) => void
5557
onContextMenu?: (event: ThreeEvent<MouseEvent>) => void
5658
onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void
59+
onDragOverEnter?: (event: ThreeEvent<DragEvent>) => void
60+
onDragOverLeave?: (event: DragEvent) => void
5761
onPointerUp?: (event: ThreeEvent<PointerEvent>) => void
5862
onPointerDown?: (event: ThreeEvent<PointerEvent>) => void
5963
onPointerOver?: (event: ThreeEvent<PointerEvent>) => void
@@ -109,6 +113,7 @@ export function getEventPriority() {
109113
case 'pointerdown':
110114
case 'pointerup':
111115
return DiscreteEventPriority
116+
case 'dragover':
112117
case 'pointermove':
113118
case 'pointerout':
114119
case 'pointerover':
@@ -171,10 +176,12 @@ export function createEvents(store: UseBoundStore<RootState>) {
171176

172177
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
173178
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-
),
179+
return objects.filter(
180+
(obj) =>
181+
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
182+
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
183+
) ||
184+
['Over'].some((name) => (obj as unknown as Instance).__r3f?.handlers[('onDrag' + name) as keyof EventHandlers]),
178185
)
179186
}
180187

@@ -377,6 +384,8 @@ export function createEvents(store: UseBoundStore<RootState>) {
377384
const data = { ...hoveredObj, intersections }
378385
handlers.onPointerOut?.(data as ThreeEvent<PointerEvent>)
379386
handlers.onPointerLeave?.(data as ThreeEvent<PointerEvent>)
387+
// @ts-ignore
388+
handlers.onDragOverLeave?.(data)
380389
}
381390
}
382391
})
@@ -409,6 +418,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
409418

410419
// Get fresh intersects
411420
const isPointerMove = name === 'onPointerMove'
421+
const isDragOver = name === 'onDragOverEnter' || name === 'onDragOverLeave'
412422
const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick'
413423
const filter = isPointerMove ? filterPointerEvents : undefined
414424
//const hits = patchIntersects(intersect(filter), event)
@@ -430,7 +440,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
430440
}
431441
}
432442
// Take care of unhover
433-
if (isPointerMove) cancelPointer(hits)
443+
if (isPointerMove || isDragOver) cancelPointer(hits)
434444

435445
handleIntersects(hits, event, delta, (data: ThreeEvent<DomEvent>) => {
436446
const eventObject = data.eventObject
@@ -457,6 +467,18 @@ export function createEvents(store: UseBoundStore<RootState>) {
457467
}
458468
// Call mouse move
459469
handlers.onPointerMove?.(data as ThreeEvent<PointerEvent>)
470+
} else if (isDragOver) {
471+
// When enter or out is present take care of hover-state
472+
const id = makeId(data)
473+
const hoveredItem = internal.hovered.get(id)
474+
if (!hoveredItem) {
475+
// If the object wasn't previously hovered, book it and call its handler
476+
internal.hovered.set(id, data)
477+
handlers.onDragOverEnter?.(data as ThreeEvent<DragEvent>)
478+
} else if (hoveredItem.stopped) {
479+
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
480+
data.stopPropagation()
481+
}
460482
} else {
461483
// All other events ...
462484
const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent<PointerEvent>) => void

packages/fiber/src/core/utils.ts

+2-1
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|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/events.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const DOM_EVENTS = {
66
onClick: ['click', false],
77
onContextMenu: ['contextmenu', false],
88
onDoubleClick: ['dblclick', false],
9+
onDragOverEnter: ['dragover', true],
10+
onDragOverLeave: ['dragover', true],
911
onWheel: ['wheel', true],
1012
onPointerDown: ['pointerdown', true],
1113
onPointerUp: ['pointerup', true],

packages/fiber/tests/core/events.test.tsx

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { render, fireEvent, RenderResult } from '@testing-library/react'
2+
import { render, fireEvent, createEvent, RenderResult } from '@testing-library/react'
33

44
import { Canvas, act } from '../../src'
55

@@ -210,6 +210,44 @@ describe('events', () => {
210210
expect(handlePointerOut).toHaveBeenCalled()
211211
})
212212

213+
it('can handle onDragOverEnter & onDragOverLeave', async () => {
214+
const handleDragOverEnter = jest.fn()
215+
const handleDragOverLeave = jest.fn()
216+
217+
await act(async () => {
218+
render(
219+
<Canvas>
220+
<mesh onDragOverEnter={handleDragOverEnter} onDragOverLeave={handleDragOverLeave}>
221+
<boxGeometry args={[2, 2]} />
222+
<meshBasicMaterial />
223+
</mesh>
224+
</Canvas>,
225+
)
226+
})
227+
228+
// Note: DragEvent is not implemented in jsdom yet: https://github.com/jsdom/jsdom/issues/2913
229+
// https://developer.mozilla.org/en-US/docs/Web/API/DragEvent
230+
// however, @react-testing/library does simulate it
231+
let evt = createEvent.dragOver(getContainer())
232+
//@ts-ignore
233+
evt.offsetX = 577
234+
//@ts-ignore
235+
evt.offsetY = 480
236+
237+
fireEvent(getContainer(), evt)
238+
239+
expect(handleDragOverEnter).toHaveBeenCalled()
240+
241+
// pretend we moved out over from the target
242+
//@ts-ignore
243+
evt.offsetX = 1
244+
//@ts-ignore
245+
evt.offsetY = 1
246+
fireEvent(getContainer(), evt)
247+
248+
expect(handleDragOverLeave).toHaveBeenCalled()
249+
})
250+
213251
it('should handle stopPropagation', async () => {
214252
const handlePointerEnter = jest.fn().mockImplementation((e) => {
215253
expect(() => e.stopPropagation()).not.toThrow()

0 commit comments

Comments
 (0)