Skip to content
Closed
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
74 changes: 74 additions & 0 deletions packages/core/src/useElementByPoint/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { act, renderHook } from '@testing-library/react'
import { useElementByPoint } from '.'

function patchRaf() {
let callbacks: Array<(t: number) => void> = []
;(global as any).requestAnimationFrame = (cb: (t: number) => void) => {
callbacks.push(cb)
return callbacks.length
}
;(global as any).cancelAnimationFrame = jest.fn()
return {
flush() {
const cbs = callbacks
callbacks = []
cbs.forEach(cb => cb(0))
},
}
}

describe('useElementByPoint', () => {
it('reports the element under the point', () => {
const raf = patchRaf()
const el = document.createElement('div')
const doc = { elementFromPoint: () => el } as unknown as Document

const { result } = renderHook(() => useElementByPoint({ x: 1, y: 2, document: doc }))

act(() => { raf.flush() })
expect(result.current.element).toBe(el)
})

it('does not re-render while the hit list is unchanged in multiple mode', () => {
const raf = patchRaf()
const el = document.createElement('div')
// A real document returns a new array on every call, even when the pointer
// has not moved.
const doc = { elementsFromPoint: () => [el] } as unknown as Document

let renders = 0
renderHook(() => {
renders++
return useElementByPoint({ x: 1, y: 2, document: doc, multiple: true })
})

// First frame legitimately moves the state from null to the hit list.
act(() => { raf.flush() })
const settled = renders

act(() => { raf.flush() })
act(() => { raf.flush() })
act(() => { raf.flush() })

expect(renders).toBe(settled)
})

it('re-renders when the hit list actually changes', () => {
const raf = patchRaf()
const first = document.createElement('div')
const second = document.createElement('span')
let hit = [first]
const doc = { elementsFromPoint: () => [...hit] } as unknown as Document

const { result } = renderHook(() =>
useElementByPoint({ x: 1, y: 2, document: doc, multiple: true }),
)

act(() => { raf.flush() })
expect(result.current.element).toEqual([first])

hit = [second, first]
act(() => { raf.flush() })
expect(result.current.element).toEqual([second, first])
})
})
16 changes: 13 additions & 3 deletions packages/core/src/useElementByPoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ export const useElementByPoint: UseElementByPoint = options => {

const cb = useCallback(() => {
const { x: currentX, y: currentY } = getXY()
setElement(
const next = multiple
? doc?.elementsFromPoint(currentX, currentY) ?? []
: doc?.elementFromPoint(currentX, currentY) ?? null
// elementsFromPoint allocates a fresh array on every call, so storing it
// as-is would re-render on every frame of the rAF loop even while the
// pointer sits still. The single-element branch needs no such check —
// elementFromPoint returns the same node and React bails out on its own.
setElement((prev: any) =>
multiple
? doc?.elementsFromPoint(currentX, currentY) ?? []
: doc?.elementFromPoint(currentX, currentY) ?? null,
&& Array.isArray(prev)
&& prev.length === next.length
&& prev.every((el: Element, i: number) => el === next[i])
? prev
: next,
)
}, [doc, multiple, getXY])

Expand Down
Loading