"use client" /** * Whether an element is currently scrolling its content. * * Exists because "is this scrollable" decides an accessibility fact, not a * cosmetic one: axe's `scrollable-region-focusable` wants a scrolling box in the * Tab order so a keyboard user can reach the content Safari will not scroll for * them, and a box that fits must stay out of it or every short list becomes a * focus stop before its own controls. That answer changes with content, zoom, and * container width, so it is observed rather than assumed. * * Shared by `ScrollRegion` and `ScrollArea`, which put the answer on different * elements: `ScrollRegion` is the scrolling box, while `ScrollArea`'s box belongs * to Radix and can only be reached by ref. */ import * as React from "react" /** Watch the box and its direct children — a child growing changes overflow too. */ function observeTree(ro: ResizeObserver, el: HTMLElement) { ro.disconnect() ro.observe(el) for (const child of el.children) { ro.observe(child) } } export function useScrollOverflow(): { ref: React.RefObject overflowing: boolean } { const ref = React.useRef(null) const [overflowing, setOverflowing] = React.useState(false) React.useLayoutEffect(() => { const el = ref.current if (!el) return const sync = () => { setOverflowing( el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1, ) } const ro = new ResizeObserver(sync) observeTree(ro, el) sync() el.addEventListener("scroll", sync, { passive: true }) const mo = new MutationObserver(() => { observeTree(ro, el) sync() }) mo.observe(el, { childList: true, subtree: true }) return () => { ro.disconnect() mo.disconnect() el.removeEventListener("scroll", sync) } }, []) return { ref, overflowing } }