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

fix: pointer events and pointer capture #2600

Merged
merged 2 commits into from Oct 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/fiber/src/core/events.ts
Expand Up @@ -252,7 +252,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
// If the interaction is captured, make all capturing targets part of the intersect.
if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
for (let captureData of state.internal.capturedMap.get(event.pointerId)!.values()) {
intersections.push(captureData.intersection)
if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection)
}
}
return intersections
Expand Down Expand Up @@ -403,7 +403,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
case 'onLostPointerCapture':
return (event: DomEvent) => {
const { internal } = store.getState()
if ('pointerId' in event && !internal.capturedMap.has(event.pointerId)) {
if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
// If the object event interface had onLostPointerCapture, we'd call it here on every
// object that's getting removed.
internal.capturedMap.delete(event.pointerId)
Expand Down
63 changes: 61 additions & 2 deletions packages/fiber/tests/core/events.test.tsx
Expand Up @@ -270,13 +270,16 @@ describe('events', () => {
describe('web pointer capture', () => {
const handlePointerMove = jest.fn()
const handlePointerDown = jest.fn((ev) => (ev.target as any).setPointerCapture(ev.pointerId))
const handlePointerUp = jest.fn((ev) => (ev.target as any).releasePointerCapture(ev.pointerId))
const handlePointerEnter = jest.fn()
const handlePointerLeave = jest.fn()

/* This component lets us unmount the event-handling object */
function PointerCaptureTest(props: { hasMesh: boolean }) {
function PointerCaptureTest(props: { hasMesh: boolean, manualRelease?: boolean }) {
return (
<Canvas>
{props.hasMesh && (
<mesh onPointerDown={handlePointerDown} onPointerMove={handlePointerMove}>
<mesh onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={props.manualRelease ? handlePointerUp : undefined} onPointerLeave={handlePointerLeave} onPointerEnter={handlePointerEnter}>
<boxGeometry args={[2, 2]} />
<meshBasicMaterial />
</mesh>
Expand Down Expand Up @@ -325,5 +328,61 @@ describe('events', () => {
/* There should now be no pointer capture */
expect(handlePointerMove).not.toHaveBeenCalled()
})

it('should not leave when captured', async () => {
let renderResult: RenderResult = undefined!
await act(async () => {
renderResult = render(<PointerCaptureTest hasMesh manualRelease />)
return renderResult
})

const canvas = getContainer()
canvas.setPointerCapture = jest.fn()
canvas.releasePointerCapture = jest.fn()

const moveIn = new PointerEvent('pointermove', { pointerId })
Object.defineProperty(moveIn, 'offsetX', { get: () => 577 })
Object.defineProperty(moveIn, 'offsetY', { get: () => 480 })

const moveOut = new PointerEvent('pointermove', { pointerId })
Object.defineProperty(moveOut, 'offsetX', { get: () => -10000 })
Object.defineProperty(moveOut, 'offsetY', { get: () => -10000 })

/* testing-utils/react's fireEvent wraps the event like React does, so it doesn't match how our event handlers are called in production, so we call dispatchEvent directly. */
await act(async () => canvas.dispatchEvent(moveIn))
expect(handlePointerEnter).toHaveBeenCalledTimes(1);
expect(handlePointerMove).toHaveBeenCalledTimes(1);

const down = new PointerEvent('pointerdown', { pointerId })
Object.defineProperty(down, 'offsetX', { get: () => 577 })
Object.defineProperty(down, 'offsetY', { get: () => 480 })

await act(async () => canvas.dispatchEvent(down))

// If we move the pointer now, when it is captured, it should raise the onPointerMove event even though the pointer is not over the element,
// and NOT raise the onPointerLeave event.
await act(async () => canvas.dispatchEvent(moveOut))
expect(handlePointerMove).toHaveBeenCalledTimes(2);
expect(handlePointerLeave).not.toHaveBeenCalled();

await act(async () => canvas.dispatchEvent(moveIn))
expect(handlePointerMove).toHaveBeenCalledTimes(3);

const up = new PointerEvent('pointerup', { pointerId })
Object.defineProperty(up, 'offsetX', { get: () => 577 })
Object.defineProperty(up, 'offsetY', { get: () => 480 })
const lostpointercapture = new PointerEvent('lostpointercapture', { pointerId })

await act(async () => canvas.dispatchEvent(up))
await act(async () => canvas.dispatchEvent(lostpointercapture))

// The pointer is still over the element, so onPointerLeave should not have been called.
expect(handlePointerLeave).not.toHaveBeenCalled();

// The element pointer should no longer be captured, so moving it away should call onPointerLeave.
await act(async () => canvas.dispatchEvent(moveOut));
expect(handlePointerEnter).toHaveBeenCalledTimes(1);
expect(handlePointerLeave).toHaveBeenCalledTimes(1)
})
})
})