From 5ce6151cb0651e1d3f96035c38abcb88969c6405 Mon Sep 17 00:00:00 2001 From: erikwu <40494306+erikkkwu@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:07:22 +0800 Subject: [PATCH] fix(useInfiniteScroll): improve visibility check (#3212) --- packages/core/useInfiniteScroll/index.test.ts | 73 +++++++++++++++++++ packages/core/useInfiniteScroll/index.ts | 27 +++++-- 2 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 packages/core/useInfiniteScroll/index.test.ts diff --git a/packages/core/useInfiniteScroll/index.test.ts b/packages/core/useInfiniteScroll/index.test.ts new file mode 100644 index 00000000000..21e79f2106f --- /dev/null +++ b/packages/core/useInfiniteScroll/index.test.ts @@ -0,0 +1,73 @@ +import { flushPromises } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import { ref } from 'vue-demi' +import { useElementVisibility } from '../useElementVisibility' +import { useInfiniteScroll } from '.' + +vi.mock('../useElementVisibility') +describe('useInfiniteScroll', () => { + it('should be defined', () => { + expect(useInfiniteScroll).toBeDefined() + }) + + it.each([ + [ref(givenMockElement())], + [givenMockElement()], + [document], + [window], + ])('should calls the loadMore handler, when element is visible', (target) => { + const mockHandler = vi.fn() + givenElementVisibilityRefMock(true) + + useInfiniteScroll(target, mockHandler) + + expect(mockHandler).toHaveBeenCalledTimes(1) + }) + + it('should calls the loadMore handler, when element visibility state form hidden to visible', async () => { + const mockHandler = vi.fn() + const mockElement = givenMockElement() + const visibilityRefMock = givenElementVisibilityRefMock(false) + + useInfiniteScroll(mockElement, mockHandler) + + expect(mockHandler).not.toHaveBeenCalled() + + visibilityRefMock.value = true + await flushPromises() + + expect(mockHandler).toHaveBeenCalledTimes(1) + }) + + it('should call the loadMore handler, when user scrolls', async () => { + const mockElementScrollHeight = 100 + const mockHandler = vi.fn() + const mockElement = givenMockElement({ + scrollHeight: mockElementScrollHeight, + }) + givenElementVisibilityRefMock(true) + + useInfiniteScroll(mockElement, mockHandler) + mockElement.scrollTop = mockElementScrollHeight + mockElement.dispatchEvent(new Event('scroll')) + await flushPromises() + + expect(mockHandler).toHaveBeenCalledTimes(1) + }) + + function givenMockElement({ + scrollHeight = 0, + } = {}): HTMLDivElement { + const mockElement = document.createElement('div') + Object.defineProperty(mockElement, 'scrollHeight', { + value: scrollHeight, + }) + return mockElement + } + + function givenElementVisibilityRefMock(defaultValue: boolean) { + const mockVisibilityRef = ref(defaultValue) + vi.mocked(useElementVisibility).mockReturnValue(mockVisibilityRef) + return mockVisibilityRef + } +}) diff --git a/packages/core/useInfiniteScroll/index.ts b/packages/core/useInfiniteScroll/index.ts index 7afa4876f7c..2a9a4cb1bb2 100644 --- a/packages/core/useInfiniteScroll/index.ts +++ b/packages/core/useInfiniteScroll/index.ts @@ -1,7 +1,8 @@ -import { computed, nextTick, reactive, ref, watch } from 'vue-demi' -import type { UnwrapNestedRefs } from 'vue-demi' import type { Awaitable, MaybeRefOrGetter } from '@vueuse/shared' import { toValue } from '@vueuse/shared' +import type { UnwrapNestedRefs } from 'vue-demi' +import { computed, nextTick, reactive, ref, watch } from 'vue-demi' +import { useElementVisibility } from '../useElementVisibility' import type { UseScrollOptions } from '../useScroll' import { useScroll } from '../useScroll' @@ -56,17 +57,29 @@ export function useInfiniteScroll( const promise = ref() const isLoading = computed(() => !!promise.value) + // Document and Window cannot be observed by IntersectionObserver + const observedElement = computed(() => { + const el = toValue(element) + if (el instanceof Window) + return window.document.documentElement + + if (el instanceof Document) + return document.documentElement + + return el + }) + const isElementVisible = useElementVisibility(observedElement) function checkAndLoad() { state.measure() - const el = toValue(element) as HTMLElement - if (!el || !el.offsetParent) + if (!observedElement.value || !isElementVisible.value) return + const { scrollHeight, clientHeight, scrollWidth, clientWidth } = observedElement.value as HTMLElement const isNarrower = (direction === 'bottom' || direction === 'top') - ? el.scrollHeight <= el.clientHeight - : el.scrollWidth <= el.clientWidth + ? scrollHeight <= clientHeight + : scrollWidth <= clientWidth if (state.arrivedState[direction] || isNarrower) { if (!promise.value) { @@ -83,7 +96,7 @@ export function useInfiniteScroll( } watch( - () => [state.arrivedState[direction], toValue(element)], + () => [state.arrivedState[direction], isElementVisible.value], checkAndLoad, { immediate: true }, )