d4b29db8c7
Fixed lazyRoot functionality (#33290). Changed the unique id for Intersection Observers discrimination since previously they were only identified by the different rootMargins, now each being identified by the rootMargin and the root element as well Added more Images to the test with different margins and with/without lazyRoot prop. Browser correctly initially loading two of the four Images according to the props' specifications. Co-authored-by: Steven <steven@ceriously.com>
143 lines
3.5 KiB
TypeScript
143 lines
3.5 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import {
|
|
requestIdleCallback,
|
|
cancelIdleCallback,
|
|
} from './request-idle-callback'
|
|
|
|
type UseIntersectionObserverInit = Pick<
|
|
IntersectionObserverInit,
|
|
'rootMargin' | 'root'
|
|
>
|
|
|
|
type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & {
|
|
rootRef?: React.RefObject<HTMLElement> | null
|
|
}
|
|
type ObserveCallback = (isVisible: boolean) => void
|
|
type Identifier = {
|
|
root: Element | Document | null
|
|
margin: string
|
|
}
|
|
type Observer = {
|
|
id: Identifier
|
|
observer: IntersectionObserver
|
|
elements: Map<Element, ObserveCallback>
|
|
}
|
|
|
|
const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined'
|
|
|
|
export function useIntersection<T extends Element>({
|
|
rootRef,
|
|
rootMargin,
|
|
disabled,
|
|
}: UseIntersection): [(element: T | null) => void, boolean] {
|
|
const isDisabled: boolean = disabled || !hasIntersectionObserver
|
|
|
|
const unobserve = useRef<Function>()
|
|
const [visible, setVisible] = useState(false)
|
|
const [root, setRoot] = useState(rootRef ? rootRef.current : null)
|
|
const setRef = useCallback(
|
|
(el: T | null) => {
|
|
if (unobserve.current) {
|
|
unobserve.current()
|
|
unobserve.current = undefined
|
|
}
|
|
|
|
if (isDisabled || visible) return
|
|
|
|
if (el && el.tagName) {
|
|
unobserve.current = observe(
|
|
el,
|
|
(isVisible) => isVisible && setVisible(isVisible),
|
|
{ root, rootMargin }
|
|
)
|
|
}
|
|
},
|
|
[isDisabled, root, rootMargin, visible]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!hasIntersectionObserver) {
|
|
if (!visible) {
|
|
const idleCallback = requestIdleCallback(() => setVisible(true))
|
|
return () => cancelIdleCallback(idleCallback)
|
|
}
|
|
}
|
|
}, [visible])
|
|
|
|
useEffect(() => {
|
|
if (rootRef) setRoot(rootRef.current)
|
|
}, [rootRef])
|
|
return [setRef, visible]
|
|
}
|
|
|
|
function observe(
|
|
element: Element,
|
|
callback: ObserveCallback,
|
|
options: UseIntersectionObserverInit
|
|
): () => void {
|
|
const { id, observer, elements } = createObserver(options)
|
|
elements.set(element, callback)
|
|
|
|
observer.observe(element)
|
|
return function unobserve(): void {
|
|
elements.delete(element)
|
|
observer.unobserve(element)
|
|
|
|
// Destroy observer when there's nothing left to watch:
|
|
if (elements.size === 0) {
|
|
observer.disconnect()
|
|
observers.delete(id)
|
|
let index = idList.findIndex(
|
|
(obj) => obj.root === id.root && obj.margin === id.margin
|
|
)
|
|
if (index > -1) {
|
|
idList.splice(index, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const observers = new Map<Identifier, Observer>()
|
|
|
|
const idList: Identifier[] = []
|
|
|
|
function createObserver(options: UseIntersectionObserverInit): Observer {
|
|
const id = {
|
|
root: options.root || null,
|
|
margin: options.rootMargin || '',
|
|
}
|
|
let existing = idList.find(
|
|
(obj) => obj.root === id.root && obj.margin === id.margin
|
|
)
|
|
let instance
|
|
if (existing) {
|
|
instance = observers.get(existing)
|
|
} else {
|
|
instance = observers.get(id)
|
|
idList.push(id)
|
|
}
|
|
if (instance) {
|
|
return instance
|
|
}
|
|
|
|
const elements = new Map<Element, ObserveCallback>()
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
const callback = elements.get(entry.target)
|
|
const isVisible = entry.isIntersecting || entry.intersectionRatio > 0
|
|
if (callback && isVisible) {
|
|
callback(isVisible)
|
|
}
|
|
})
|
|
}, options)
|
|
|
|
observers.set(
|
|
id,
|
|
(instance = {
|
|
id,
|
|
observer,
|
|
elements,
|
|
})
|
|
)
|
|
return instance
|
|
}
|