// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useScrollOverflow.ts * @input Uses React useRef, useState, useCallback, useEffect * @output Exports useScrollOverflow hook for tracking scroll position edges * @position Core hook; used by Carousel for fade-edge and button state * * Observes a scroll container and reports whether content overflows at the * start, end, or both edges. Updates on scroll and resize. * * SYNC: When modified, update: * - /packages/core/src/hooks/index.ts */ import { useState, useCallback, useEffect, useRef } from 'react'; import { observeResize } from "../utils/sharedResizeObserver.js"; export interface ScrollOverflowState { /** Content overflows the start (left in LTR, right in RTL) */ overflowStart: boolean; /** Content overflows the end (right in LTR, left in RTL) */ overflowEnd: boolean; /** Whether the container has any overflow at all */ hasOverflow: boolean; } /** * Tracks scroll overflow state for a horizontally scrollable container. * * Returns a ref callback to attach to the scroll container and a state * object that updates as the user scrolls or the container resizes. * * @example * ``` * const { scrollRef, overflowStart, overflowEnd } = useScrollOverflow(); *
...
* ``` */ export function useScrollOverflow() { const elRef = useRef(null); const unobserveRef = useRef<(() => void) | null>(null); const [state, setState] = useState({ overflowStart: false, overflowEnd: false, hasOverflow: false }); const measure = useCallback(() => { const el = elRef.current; if (!el) { return; } const tolerance = 1; const { scrollLeft, scrollWidth, clientWidth } = el; const maxScroll = scrollWidth - clientWidth; const overflowStart = Math.abs(scrollLeft) > tolerance; const overflowEnd = Math.abs(scrollLeft) < maxScroll - tolerance; const hasOverflow = scrollWidth > clientWidth + tolerance; setState(prev => { if (prev.overflowStart === overflowStart && prev.overflowEnd === overflowEnd && prev.hasOverflow === hasOverflow) { return prev; } return { overflowStart, overflowEnd, hasOverflow }; }); }, []); const scrollRef = useCallback((el: HTMLElement | null) => { if (elRef.current) { unobserveRef.current?.(); unobserveRef.current = null; elRef.current.removeEventListener('scroll', measure); } elRef.current = el; if (el) { el.addEventListener('scroll', measure, { passive: true }); unobserveRef.current = observeResize(el, measure); } }, [measure]); useEffect(() => { return () => { if (elRef.current) { unobserveRef.current?.(); unobserveRef.current = null; elRef.current.removeEventListener('scroll', measure); } }; }, [measure]); return { scrollRef, ...state }; }