167 lines
3.9 KiB
TypeScript
167 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { useThrottledCallback } from "./use-throttled-callback"
|
|
|
|
export type RectState = Omit<DOMRect, "toJSON">
|
|
|
|
export interface ElementRectOptions {
|
|
/**
|
|
* The element to track. Can be an Element, ref, or selector string.
|
|
* Defaults to document.body if not provided.
|
|
*/
|
|
element?: Element | React.RefObject<Element> | string | null
|
|
/**
|
|
* Whether to enable rect tracking
|
|
*/
|
|
enabled?: boolean
|
|
/**
|
|
* Throttle delay in milliseconds for rect updates
|
|
*/
|
|
throttleMs?: number
|
|
/**
|
|
* Whether to use ResizeObserver for more accurate tracking
|
|
*/
|
|
useResizeObserver?: boolean
|
|
}
|
|
|
|
const initialRect: RectState = {
|
|
x: 0,
|
|
y: 0,
|
|
width: 0,
|
|
height: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
}
|
|
|
|
const isSSR = typeof window === "undefined"
|
|
const hasResizeObserver = !isSSR && typeof ResizeObserver !== "undefined"
|
|
|
|
/**
|
|
* Helper function to check if code is running on client side
|
|
*/
|
|
const isClientSide = (): boolean => !isSSR
|
|
|
|
/**
|
|
* Custom hook that tracks an element's bounding rectangle and updates on resize, scroll, etc.
|
|
*
|
|
* @param options Configuration options for element rect tracking
|
|
* @returns The current bounding rectangle of the element
|
|
*/
|
|
export function useElementRect({
|
|
element,
|
|
enabled = true,
|
|
throttleMs = 100,
|
|
useResizeObserver = true,
|
|
}: ElementRectOptions = {}): RectState {
|
|
const [rect, setRect] = React.useState<RectState>(initialRect)
|
|
|
|
const getTargetElement = React.useCallback((): Element | null => {
|
|
if (!enabled || !isClientSide()) return null
|
|
|
|
if (!element) {
|
|
return document.body
|
|
}
|
|
|
|
if (typeof element === "string") {
|
|
return document.querySelector(element)
|
|
}
|
|
|
|
if ("current" in element) {
|
|
return element.current
|
|
}
|
|
|
|
return element
|
|
}, [element, enabled])
|
|
|
|
const updateRect = useThrottledCallback(
|
|
() => {
|
|
if (!enabled || !isClientSide()) return
|
|
|
|
const targetElement = getTargetElement()
|
|
if (!targetElement) {
|
|
setRect(initialRect)
|
|
return
|
|
}
|
|
|
|
const newRect = targetElement.getBoundingClientRect()
|
|
setRect({
|
|
x: newRect.x,
|
|
y: newRect.y,
|
|
width: newRect.width,
|
|
height: newRect.height,
|
|
top: newRect.top,
|
|
right: newRect.right,
|
|
bottom: newRect.bottom,
|
|
left: newRect.left,
|
|
})
|
|
},
|
|
throttleMs,
|
|
[enabled, getTargetElement],
|
|
{ leading: true, trailing: true }
|
|
)
|
|
|
|
React.useEffect(() => {
|
|
if (!enabled || !isClientSide()) {
|
|
setRect(initialRect)
|
|
return
|
|
}
|
|
|
|
const targetElement = getTargetElement()
|
|
if (!targetElement) return
|
|
|
|
updateRect()
|
|
|
|
const cleanup: (() => void)[] = []
|
|
|
|
if (useResizeObserver && hasResizeObserver) {
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
window.requestAnimationFrame(updateRect)
|
|
})
|
|
resizeObserver.observe(targetElement)
|
|
cleanup.push(() => resizeObserver.disconnect())
|
|
}
|
|
|
|
const handleUpdate = () => updateRect()
|
|
|
|
window.addEventListener("scroll", handleUpdate, { passive: true })
|
|
window.addEventListener("resize", handleUpdate, { passive: true })
|
|
|
|
cleanup.push(() => {
|
|
window.removeEventListener("scroll", handleUpdate)
|
|
window.removeEventListener("resize", handleUpdate)
|
|
})
|
|
|
|
return () => {
|
|
cleanup.forEach((fn) => fn())
|
|
setRect(initialRect)
|
|
}
|
|
}, [enabled, getTargetElement, updateRect, useResizeObserver])
|
|
|
|
return rect
|
|
}
|
|
|
|
/**
|
|
* Convenience hook for tracking document.body rect
|
|
*/
|
|
export function useBodyRect(
|
|
options: Omit<ElementRectOptions, "element"> = {}
|
|
): RectState {
|
|
return useElementRect({
|
|
...options,
|
|
element: isClientSide() ? document.body : null,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Convenience hook for tracking a ref element's rect
|
|
*/
|
|
export function useRefRect<T extends Element>(
|
|
ref: React.RefObject<T>,
|
|
options: Omit<ElementRectOptions, "element"> = {}
|
|
): RectState {
|
|
return useElementRect({ ...options, element: ref })
|
|
}
|