Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for file drag & drop events #2346

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/API/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ nav: 8

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)`.

Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes.
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.

```jsx
<mesh
onClick={(e) => console.log('click')}
onContextMenu={(e) => console.log('context menu')}
onDoubleClick={(e) => console.log('double click')}
onDragEnter={(e) => console.log('drag enter')}
onDragLeave={(e) => console.log('drag leave')}
onDragOverEnter={(e) => console.log('dragover enter')}
onDragOverLeave={(e) => console.log('dragover leave')}
onDragOverMissed={(e) => console.log('dragover missed')}
onDrop={(e) => console.log('dropped')}
onDropMissed={(e) => console.log('drop missed')}
onWheel={(e) => console.log('wheel spins')}
onPointerUp={(e) => console.log('up')}
onPointerDown={(e) => console.log('down')}
Expand Down
53 changes: 53 additions & 0 deletions example/src/demos/FileDragDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { SyntheticEvent, useState } from 'react'
import { Canvas } from '@react-three/fiber'
import { a, useSpring } from '@react-spring/three'
import { OrbitControls } from '@react-three/drei'

export default function FileDragDrop() {
const [active, setActive] = useState(0)
const [activeBg, setActiveBg] = useState(0)
// create a common spring that will be used later to interpolate other values
const { spring } = useSpring({
spring: active,
config: { mass: 5, tension: 400, friction: 50, precision: 0.0001 },
})
// interpolate values from commong spring
const scale = spring.to([0, 1], [1, 2])
const rotation = spring.to([0, 1], [0, Math.PI])
const color = active ? spring.to([0, 1], ['#6246ea', '#e45858']) : spring.to([0, 1], ['#620000', '#e40000'])
const bgColor = activeBg ? 'lightgreen' : 'lightgray'
const preventDragDropDefaults = {
onDrop: (e: SyntheticEvent) => e.preventDefault(),
onDragEnter: (e: SyntheticEvent) => e.preventDefault(),
onDragOver: (e: SyntheticEvent) => e.preventDefault(),
}
return (
<Canvas
{...preventDragDropDefaults}
onDropMissed={(e) => {
console.log('drop missed!')
setActiveBg(0)
}}
onDragOverMissed={(e) => setActiveBg(1)}
onDragLeave={() => setActiveBg(0)}>
<color attach="background" args={[bgColor]} />
<a.mesh
rotation-y={rotation}
scale-x={scale}
scale-z={scale}
onDrop={(e) => {
console.log('dropped!')
setActive(0)
}}
onDragOverEnter={() => {
setActive(1)
setActiveBg(0)
}}
onDragOverLeave={() => setActive(0)}>
<boxBufferGeometry />
<a.meshBasicMaterial color={color} />
</a.mesh>
<OrbitControls />
</Canvas>
)
}
2 changes: 2 additions & 0 deletions example/src/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Animation = { Component: lazy(() => import('./Animation')) }
const AutoDispose = { Component: lazy(() => import('./AutoDispose')) }
const ClickAndHover = { Component: lazy(() => import('./ClickAndHover')) }
const ContextMenuOverride = { Component: lazy(() => import('./ContextMenuOverride')) }
const FileDragDrop = { Component: lazy(() => import('./FileDragDrop')) }
const Gestures = { Component: lazy(() => import('./Gestures')) }
const Gltf = { Component: lazy(() => import('./Gltf')) }
const Inject = { Component: lazy(() => import('./Inject')) }
Expand All @@ -30,6 +31,7 @@ export {
AutoDispose,
ClickAndHover,
ContextMenuOverride,
FileDragDrop,
Gestures,
Gltf,
Inject,
Expand Down
77 changes: 71 additions & 6 deletions packages/fiber/src/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export type Events = {
onClick: EventListener
onContextMenu: EventListener
onDoubleClick: EventListener
onDragEnter: EventListener
onDragLeave: EventListener
onDragOverEnter: EventListener
onDragOverLeave: EventListener
onDrop: EventListener
onDropMissed: EventListener
onWheel: EventListener
onPointerDown: EventListener
onPointerUp: EventListener
Expand All @@ -54,6 +60,13 @@ export type EventHandlers = {
onClick?: (event: ThreeEvent<MouseEvent>) => void
onContextMenu?: (event: ThreeEvent<MouseEvent>) => void
onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void
onDragEnter?: (event: ThreeEvent<DragEvent>) => void
onDragLeave?: (event: ThreeEvent<DragEvent>) => void
onDragOverEnter?: (event: ThreeEvent<DragEvent>) => void
onDragOverLeave?: (event: ThreeEvent<DragEvent>) => void
onDragOverMissed?: (event: DragEvent) => void
onDrop?: (event: ThreeEvent<DragEvent>) => void
onDropMissed?: (event: DragEvent) => void
onPointerUp?: (event: ThreeEvent<PointerEvent>) => void
onPointerDown?: (event: ThreeEvent<PointerEvent>) => void
onPointerOver?: (event: ThreeEvent<PointerEvent>) => void
Expand Down Expand Up @@ -110,10 +123,14 @@ export function getEventPriority() {
case 'click':
case 'contextmenu':
case 'dblclick':
case 'dragenter':
case 'dragleave':
case 'drop':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
return DiscreteEventPriority
case 'dragover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
Expand Down Expand Up @@ -174,10 +191,14 @@ export function createEvents(store: UseBoundStore<RootState>) {

/** 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', 'Enter', 'Leave'].some(
(name) => (obj as unknown as Instance).__r3f?.handlers[('onDrag' + name) as keyof EventHandlers],
),
)
}

Expand Down Expand Up @@ -382,6 +403,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
const data = { ...hoveredObj, intersections }
handlers.onPointerOut?.(data as ThreeEvent<PointerEvent>)
handlers.onPointerLeave?.(data as ThreeEvent<PointerEvent>)
handlers.onDragOverLeave?.(data as ThreeEvent<DragEvent>)
}
}
}
Expand All @@ -394,11 +416,26 @@ export function createEvents(store: UseBoundStore<RootState>) {
}
}

function dragOverMissed(event: DragEvent, objects: THREE.Object3D[]) {
for (let i = 0; i < objects.length; i++) {
const instance = (objects[i] as unknown as Instance).__r3f
instance?.handlers.onDragOverMissed?.(event)
}
}

function dropMissed(event: DragEvent, objects: THREE.Object3D[]) {
for (let i = 0; i < objects.length; i++) {
const instance = (objects[i] as unknown as Instance).__r3f
instance?.handlers.onDropMissed?.(event)
}
}

function handlePointer(name: string) {
// Deal with cancelation
switch (name) {
case 'onPointerLeave':
case 'onPointerCancel':
case 'onDragLeave':
return () => cancelPointer([])
case 'onLostPointerCapture':
return (event: DomEvent) => {
Expand All @@ -414,13 +451,15 @@ export function createEvents(store: UseBoundStore<RootState>) {

// Any other pointer goes here ...
return function handleEvent(event: DomEvent) {
const { onPointerMissed, internal } = store.getState()
const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = store.getState()

// prepareRay(event)
internal.lastEvent.current = event

// Get fresh intersects
const isPointerMove = name === 'onPointerMove'
const isDragOver = name === 'onDragOver'
const isDrop = name === 'onDrop'
const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick'
const filter = isPointerMove ? filterPointerEvents : undefined
// const hits = patchIntersects(intersect(filter), event)
Expand All @@ -441,8 +480,17 @@ export function createEvents(store: UseBoundStore<RootState>) {
if (onPointerMissed) onPointerMissed(event)
}
}
if (isDragOver && !hits.length) {
dragOverMissed(event as DragEvent, internal.interaction)
if (onDragOverMissed) onDragOverMissed(event as DragEvent)
}
if (isDrop && !hits.length) {
dropMissed(event as DragEvent, internal.interaction)
if (onDropMissed) onDropMissed(event as DragEvent)
}

// Take care of unhover
if (isPointerMove) cancelPointer(hits)
if (isPointerMove || isDragOver) cancelPointer(hits)

function onIntersect(data: ThreeEvent<DomEvent>) {
const eventObject = data.eventObject
Expand All @@ -469,6 +517,23 @@ export function createEvents(store: UseBoundStore<RootState>) {
}
// Call mouse move
handlers.onPointerMove?.(data as ThreeEvent<PointerEvent>)
} 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<DragEvent>)
} else if (hoveredItem.stopped) {
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
data.stopPropagation()
} else if (internal.initialHits.includes(eventObject)) {
dragOverMissed(
event as DragEvent,
internal.interaction.filter((object) => !internal.initialHits.includes(object)),
)
}
} else {
// All other events ...
const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent<PointerEvent>) => void
Expand Down
10 changes: 10 additions & 0 deletions packages/fiber/src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export type RenderProps<TCanvas extends Element> = {
onCreated?: (state: RootState) => void
/** Response for pointer clicks that have missed any target */
onPointerMissed?: (event: MouseEvent) => void
/** Response for dragover events that have missed any target */
onDragOverMissed?: (event: DragEvent) => void
/** Response for drop events that have missed any target */
onDropMissed?: (event: DragEvent) => void
}

const createRendererInstance = <TElement extends Element>(gl: GLProps, canvas: TElement): THREE.WebGLRenderer => {
Expand Down Expand Up @@ -188,6 +192,8 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
raycaster: raycastOptions,
camera: cameraOptions,
onPointerMissed,
onDragOverMissed,
onDropMissed,
} = props

let state = store.getState()
Expand Down Expand Up @@ -303,6 +309,10 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
if (state.frameloop !== frameloop) state.setFrameloop(frameloop)
// Check pointer missed
if (!state.onPointerMissed) state.set({ onPointerMissed })
// Check dragover missed
if (!state.onDragOverMissed) state.set({ onDragOverMissed })
// Check drop missed
if (!state.onDropMissed) state.set({ onDropMissed })
// Check performance
if (performance && !is.equ(performance, state.performance, shallowLoose))
state.set((state) => ({ performance: { ...state.performance, ...performance } }))
Expand Down
6 changes: 6 additions & 0 deletions packages/fiber/src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ export type RootState = {
setFrameloop: (frameloop?: 'always' | 'demand' | 'never') => void
/** When the canvas was clicked but nothing was hit */
onPointerMissed?: (event: MouseEvent) => void
/** When the canvas was dragover but nothing was hit */
onDragOverMissed?: (event: DragEvent) => void
/** When the canvas was dropped but nothing was hit */
onDropMissed?: (event: DragEvent) => void
/** If this state model is layerd (via createPortal) then this contains the previous layer */
previousRoot?: UseBoundStore<RootState, StoreApi<RootState>>
/** Internals */
Expand Down Expand Up @@ -213,6 +217,8 @@ const createStore = (

frameloop: 'always',
onPointerMissed: undefined,
onDragOverMissed: undefined,
onDropMissed: undefined,

performance: {
current: 1,
Expand Down
3 changes: 2 additions & 1 deletion packages/fiber/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,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|Drop|Click|DoubleClick|ContextMenu|Wheel)/.test(key))
return changes.push([key, value, true, []])
// Split dashed props
let entries: string[] = []
if (key.includes('-')) entries = key.split('-')
Expand Down
6 changes: 6 additions & 0 deletions packages/fiber/src/web/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(func
raycaster,
camera,
onPointerMissed,
onDragOverMissed,
onDropMissed,
onCreated,
...props
},
Expand All @@ -63,6 +65,8 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(func
React.useImperativeHandle(forwardedRef, () => canvasRef.current)

const handlePointerMissed = useMutableCallback(onPointerMissed)
const handleDragOverMissed = useMutableCallback(onDragOverMissed)
const handleDropMissed = useMutableCallback(onDropMissed)
const [block, setBlock] = React.useState<SetBlock>(false)
const [error, setError] = React.useState<any>(false)

Expand Down Expand Up @@ -91,6 +95,8 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(func
size: containerRect,
// Pass mutable reference to onPointerMissed so it's free to update
onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
onDropMissed: (...args) => handleDropMissed.current?.(...args),
onCreated: (state) => {
// Connect to event source
state.events.connect?.(eventSource ? (isRef(eventSource) ? eventSource.current : eventSource) : divRef.current)
Expand Down
4 changes: 4 additions & 0 deletions packages/fiber/src/web/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const DOM_EVENTS = {
onClick: ['click', false],
onContextMenu: ['contextmenu', false],
onDoubleClick: ['dblclick', false],
onDragEnter: ['dragenter', false],
onDragLeave: ['dragleave', false],
onDragOver: ['dragover', false],
onDrop: ['drop', false],
onWheel: ['wheel', true],
onPointerDown: ['pointerdown', true],
onPointerUp: ['pointerup', true],
Expand Down
Loading