diff --git a/packages/fiber/src/core/events.ts b/packages/fiber/src/core/events.ts index 749d17ca65..ae735e26a3 100644 --- a/packages/fiber/src/core/events.ts +++ b/packages/fiber/src/core/events.ts @@ -41,6 +41,8 @@ export type Events = { onClick: EventListener onContextMenu: EventListener onDoubleClick: EventListener + onDragOverEnter: EventListener + onDragOverLeave: EventListener onWheel: EventListener onPointerDown: EventListener onPointerUp: EventListener @@ -54,6 +56,8 @@ export type EventHandlers = { onClick?: (event: ThreeEvent) => void onContextMenu?: (event: ThreeEvent) => void onDoubleClick?: (event: ThreeEvent) => void + onDragOverEnter?: (event: ThreeEvent) => void + onDragOverLeave?: (event: DragEvent) => void onPointerUp?: (event: ThreeEvent) => void onPointerDown?: (event: ThreeEvent) => void onPointerOver?: (event: ThreeEvent) => void @@ -109,6 +113,7 @@ export function getEventPriority() { case 'pointerdown': case 'pointerup': return DiscreteEventPriority + case 'dragover': case 'pointermove': case 'pointerout': case 'pointerover': @@ -171,10 +176,12 @@ export function createEvents(store: UseBoundStore) { /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ function filterPointerEvents(objects: THREE.Object3D[]) { - return objects.filter((obj) => - ['Move', 'Over', 'Enter', 'Out', 'Leave'].some( - (name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers], - ), + return objects.filter( + (obj) => + ['Move', 'Over', 'Enter', 'Out', 'Leave'].some( + (name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers], + ) || + ['Over'].some((name) => (obj as unknown as Instance).__r3f?.handlers[('onDrag' + name) as keyof EventHandlers]), ) } @@ -377,6 +384,8 @@ export function createEvents(store: UseBoundStore) { const data = { ...hoveredObj, intersections } handlers.onPointerOut?.(data as ThreeEvent) handlers.onPointerLeave?.(data as ThreeEvent) + // @ts-ignore + handlers.onDragOverLeave?.(data) } } }) @@ -409,6 +418,7 @@ export function createEvents(store: UseBoundStore) { // Get fresh intersects const isPointerMove = name === 'onPointerMove' + const isDragOver = name === 'onDragOverEnter' || name === 'onDragOverLeave' const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick' const filter = isPointerMove ? filterPointerEvents : undefined //const hits = patchIntersects(intersect(filter), event) @@ -430,7 +440,7 @@ export function createEvents(store: UseBoundStore) { } } // Take care of unhover - if (isPointerMove) cancelPointer(hits) + if (isPointerMove || isDragOver) cancelPointer(hits) handleIntersects(hits, event, delta, (data: ThreeEvent) => { const eventObject = data.eventObject @@ -457,6 +467,18 @@ export function createEvents(store: UseBoundStore) { } // Call mouse move handlers.onPointerMove?.(data as ThreeEvent) + } else if (isDragOver) { + // When enter or out is present take care of hover-state + const id = makeId(data) + const hoveredItem = internal.hovered.get(id) + if (!hoveredItem) { + // If the object wasn't previously hovered, book it and call its handler + internal.hovered.set(id, data) + handlers.onDragOverEnter?.(data as ThreeEvent) + } else if (hoveredItem.stopped) { + // If the object was previously hovered and stopped, we shouldn't allow other items to proceed + data.stopPropagation() + } } else { // All other events ... const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent) => void diff --git a/packages/fiber/src/core/utils.ts b/packages/fiber/src/core/utils.ts index 6784c10eae..7468ce82d3 100644 --- a/packages/fiber/src/core/utils.ts +++ b/packages/fiber/src/core/utils.ts @@ -219,7 +219,8 @@ export function diffProps( // When props match bail out if (is.equ(value, previous[key])) return // Collect handlers and bail out - if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) return changes.push([key, value, true, []]) + if (/^on(Pointer|DragOver|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) + return changes.push([key, value, true, []]) // Split dashed props let entries: string[] = [] if (key.includes('-')) entries = key.split('-') diff --git a/packages/fiber/src/web/events.ts b/packages/fiber/src/web/events.ts index 7dbfeb8e8c..a381253c96 100644 --- a/packages/fiber/src/web/events.ts +++ b/packages/fiber/src/web/events.ts @@ -6,6 +6,8 @@ const DOM_EVENTS = { onClick: ['click', false], onContextMenu: ['contextmenu', false], onDoubleClick: ['dblclick', false], + onDragOverEnter: ['dragover', true], + onDragOverLeave: ['dragover', true], onWheel: ['wheel', true], onPointerDown: ['pointerdown', true], onPointerUp: ['pointerup', true], diff --git a/packages/fiber/tests/core/events.test.tsx b/packages/fiber/tests/core/events.test.tsx index f9efebcdde..41f82e9573 100644 --- a/packages/fiber/tests/core/events.test.tsx +++ b/packages/fiber/tests/core/events.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { render, fireEvent, RenderResult } from '@testing-library/react' +import { render, fireEvent, createEvent, RenderResult } from '@testing-library/react' import { Canvas, act } from '../../src' @@ -210,6 +210,44 @@ describe('events', () => { expect(handlePointerOut).toHaveBeenCalled() }) + it('can handle onDragOverEnter & onDragOverLeave', async () => { + const handleDragOverEnter = jest.fn() + const handleDragOverLeave = jest.fn() + + await act(async () => { + render( + + + + + + , + ) + }) + + // Note: DragEvent is not implemented in jsdom yet: https://github.com/jsdom/jsdom/issues/2913 + // https://developer.mozilla.org/en-US/docs/Web/API/DragEvent + // however, @react-testing/library does simulate it + let evt = createEvent.dragOver(getContainer()) + //@ts-ignore + evt.offsetX = 577 + //@ts-ignore + evt.offsetY = 480 + + fireEvent(getContainer(), evt) + + expect(handleDragOverEnter).toHaveBeenCalled() + + // pretend we moved out over from the target + //@ts-ignore + evt.offsetX = 1 + //@ts-ignore + evt.offsetY = 1 + fireEvent(getContainer(), evt) + + expect(handleDragOverLeave).toHaveBeenCalled() + }) + it('should handle stopPropagation', async () => { const handlePointerEnter = jest.fn().mockImplementation((e) => { expect(() => e.stopPropagation()).not.toThrow()