Skip to content

Commit

Permalink
fix(useElementVisibility)!: use useIntersectionObserver instead of sc…
Browse files Browse the repository at this point in the history
…roll event handler (#2551)
  • Loading branch information
curtgrimes committed Mar 28, 2023
1 parent a2f334d commit 74b00a0
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 106 deletions.
118 changes: 48 additions & 70 deletions packages/core/useElementVisibility/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,75 @@
import { nextTick } from 'vue-demi'
import { useElementVisibility } from '.'

describe('useElementVisibility', () => {
let el: HTMLDivElement
const overLeft = document.documentElement.clientWidth + 100
const overTop = document.documentElement.clientHeight + 100
const rect = {
y: 0,
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
} as DOMRect
function scrollTrigger() {
window.dispatchEvent(new Event('scroll'))
}
function mockGetBoundingClientRect(values: DOMRect[]) {
const mocker = values.reduce((f, result) => f.mockReturnValueOnce(result), vi.fn())
// prevent error when other tests trigger scroll
mocker.mockImplementation(() => rect)
return mocker
}

beforeEach(() => {
el = document.createElement('div')
window.innerWidth = document.documentElement.clientWidth
window.innerHeight = document.documentElement.clientHeight
document.body.appendChild(el)
})

it('should work when el is not an element', async () => {
const visible = useElementVisibility(null)
expect(visible.value).toBeFalsy()
scrollTrigger()
await nextTick()
})

it('should work when window is undefined', () => {
// @ts-expect-error set window null
const visible = useElementVisibility(el, { window: null })
expect(visible.value).toBeFalsy()
})

it('should work when scrollY', async () => {
el.getBoundingClientRect = mockGetBoundingClientRect([
rect,
{ ...rect, top: overTop },
rect,
{ ...rect, top: overTop },
])
describe('when internally using useIntersectionObserver', async () => {
const { useIntersectionObserver } = await import('../useIntersectionObserver')

const visible = useElementVisibility(el)
expect(visible.value).toBeTruthy()
beforeAll(() => {
vi.resetAllMocks()
vi.mock('../useIntersectionObserver')
})

scrollTrigger()
await nextTick()
expect(visible.value).toBeFalsy()
it('should call useIntersectionObserver internally', () => {
expect(useIntersectionObserver).toHaveBeenCalledTimes(0)
useElementVisibility(el)
expect(useIntersectionObserver).toHaveBeenCalledTimes(1)
})

window.innerHeight = 0
scrollTrigger()
await nextTick()
expect(visible.value).toBeTruthy()
it('passes the given element to useIntersectionObserver', () => {
useElementVisibility(el)
expect(vi.mocked(useIntersectionObserver).mock.lastCall?.[0]).toBe(el)
})

scrollTrigger()
await nextTick()
expect(visible.value).toBeFalsy()
})
it('passes a callback to useIntersectionObserver that sets visibility to false only when isIntersecting is false', () => {
const isVisible = useElementVisibility(el)
const callback = vi.mocked(useIntersectionObserver).mock.lastCall?.[1]
const callMockCallbackWithIsIntersectingValue = (isIntersecting: boolean) => callback?.([{ isIntersecting } as IntersectionObserverEntry], {} as IntersectionObserver)

it('should work when scrollX', async () => {
el.getBoundingClientRect = mockGetBoundingClientRect([
rect,
{ ...rect, left: overLeft },
rect,
{ ...rect, left: overLeft },
])
// It should be false initially
expect(isVisible.value).toBe(false)

const visible = useElementVisibility(el)
expect(visible.value).toBeTruthy()
// It should still be false if the callback doesn't get an isIntersecting = true
callMockCallbackWithIsIntersectingValue(false)
expect(isVisible.value).toBe(false)

scrollTrigger()
await nextTick()
expect(visible.value).toBeFalsy()
// But it should become true if the callback gets an isIntersecting = true
callMockCallbackWithIsIntersectingValue(true)
expect(isVisible.value).toBe(true)

window.innerWidth = 0
scrollTrigger()
await nextTick()
expect(visible.value).toBeTruthy()
// And it should become false again if isIntersecting = false
callMockCallbackWithIsIntersectingValue(false)
expect(isVisible.value).toBe(false)
})

scrollTrigger()
await nextTick()
expect(visible.value).toBeFalsy()
})
it('passes the given window to useIntersectionObserver', () => {
const mockWindow = {} as Window

it('should work when window is undefined', () => {
// @ts-expect-error set window null
const visible = useElementVisibility(el, { window: null })
expect(visible.value).toBeFalsy()
useElementVisibility(el, { window: mockWindow })
expect(vi.mocked(useIntersectionObserver).mock.lastCall?.[2]).toContain({ window: mockWindow })
})

it('uses the given scrollTarget as the root element in useIntersectionObserver', () => {
const mockScrollTarget = document.createElement('div')

useElementVisibility(el, { scrollTarget: mockScrollTarget })
expect(vi.mocked(useIntersectionObserver).mock.lastCall?.[2]).toContain({ root: mockScrollTarget })
})
})
})
44 changes: 11 additions & 33 deletions packages/core/useElementVisibility/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { MaybeComputedRef } from '@vueuse/shared'
import { ref, watch } from 'vue-demi'
import { ref } from 'vue-demi'
import type { MaybeComputedElementRef } from '../unrefElement'
import { unrefElement } from '../unrefElement'
import { useEventListener } from '../useEventListener'
import { useIntersectionObserver } from '../useIntersectionObserver'
import type { ConfigurableWindow } from '../_configurable'
import { defaultWindow } from '../_configurable'

Expand All @@ -23,37 +22,16 @@ export function useElementVisibility(
) {
const elementIsVisible = ref(false)

const testBounding = () => {
if (!window)
return

const document = window.document
const el = unrefElement(element)
if (!el) {
elementIsVisible.value = false
}
else {
const rect = el.getBoundingClientRect()
elementIsVisible.value = (
rect.top <= (window.innerHeight || document.documentElement.clientHeight)
&& rect.left <= (window.innerWidth || document.documentElement.clientWidth)
&& rect.bottom >= 0
&& rect.right >= 0
)
}
}

watch(
() => unrefElement(element),
() => testBounding(),
{ immediate: true, flush: 'post' },
useIntersectionObserver(
element,
([{ isIntersecting }]) => {
elementIsVisible.value = isIntersecting
},
{
root: scrollTarget,
window,
},
)

if (window) {
useEventListener(scrollTarget || window, 'scroll', testBounding, {
capture: false, passive: true,
})
}

return elementIsVisible
}
6 changes: 3 additions & 3 deletions packages/core/useIntersectionObserver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { watch } from 'vue-demi'
import { noop, tryOnScopeDispose } from '@vueuse/shared'
import type { ConfigurableWindow } from '../_configurable'
import { defaultWindow } from '../_configurable'
import type { MaybeElementRef } from '../unrefElement'
import type { MaybeComputedElementRef } from '../unrefElement'
import { unrefElement } from '../unrefElement'
import { useSupported } from '../useSupported'

export interface UseIntersectionObserverOptions extends ConfigurableWindow {
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection.
*/
root?: MaybeElementRef
root?: MaybeComputedElementRef

/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections.
Expand All @@ -32,7 +32,7 @@ export interface UseIntersectionObserverOptions extends ConfigurableWindow {
* @param options
*/
export function useIntersectionObserver(
target: MaybeElementRef,
target: MaybeComputedElementRef,
callback: IntersectionObserverCallback,
options: UseIntersectionObserverOptions = {},
) {
Expand Down

0 comments on commit 74b00a0

Please sign in to comment.