UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

180 lines 7.12 kB
import * as React from 'react'; import { useEventCallback } from '../../utils/useEventCallback.js'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect.js'; import { mergeReactProps } from '../../utils/mergeReactProps.js'; import { useBaseUiId } from '../../utils/useBaseUiId.js'; import { SCROLL_TIMEOUT } from '../constants.js'; import { getOffset } from '../utils/getOffset.js'; import { ScrollAreaRootCssVars } from './ScrollAreaRootCssVars.js'; export function useScrollAreaRoot(params) { const { dir: dirParam } = params; const [hovering, setHovering] = React.useState(false); const [scrolling, setScrolling] = React.useState(false); const [cornerSize, setCornerSize] = React.useState({ width: 0, height: 0 }); const [thumbSize, setThumbSize] = React.useState({ width: 0, height: 0 }); const [touchModality, setTouchModality] = React.useState(false); const rootId = useBaseUiId(); const viewportRef = React.useRef(null); const scrollbarYRef = React.useRef(null); const scrollbarXRef = React.useRef(null); const thumbYRef = React.useRef(null); const thumbXRef = React.useRef(null); const cornerRef = React.useRef(null); const thumbDraggingRef = React.useRef(false); const startYRef = React.useRef(0); const startXRef = React.useRef(0); const startScrollTopRef = React.useRef(0); const startScrollLeftRef = React.useRef(0); const currentOrientationRef = React.useRef('vertical'); const timeoutRef = React.useRef(-1); const [hiddenState, setHiddenState] = React.useState({ scrollbarYHidden: false, scrollbarXHidden: false, cornerHidden: false }); const [autoDir, setAutoDir] = React.useState(dirParam); const dir = dirParam ?? autoDir; useEnhancedEffect(() => { if (dirParam === undefined && viewportRef.current) { setAutoDir(getComputedStyle(viewportRef.current).direction); } }, [dirParam]); React.useEffect(() => { return () => { window.clearTimeout(timeoutRef.current); }; }, []); const handleScroll = useEventCallback(() => { setScrolling(true); window.clearTimeout(timeoutRef.current); timeoutRef.current = window.setTimeout(() => { setScrolling(false); }, SCROLL_TIMEOUT); }); const handlePointerDown = useEventCallback(event => { thumbDraggingRef.current = true; startYRef.current = event.clientY; startXRef.current = event.clientX; currentOrientationRef.current = event.currentTarget.getAttribute('data-orientation'); if (viewportRef.current) { startScrollTopRef.current = viewportRef.current.scrollTop; startScrollLeftRef.current = viewportRef.current.scrollLeft; } if (thumbYRef.current && currentOrientationRef.current === 'vertical') { thumbYRef.current.setPointerCapture(event.pointerId); } if (thumbXRef.current && currentOrientationRef.current === 'horizontal') { thumbXRef.current.setPointerCapture(event.pointerId); } }); const handlePointerMove = useEventCallback(event => { if (!thumbDraggingRef.current) { return; } const deltaY = event.clientY - startYRef.current; const deltaX = event.clientX - startXRef.current; if (viewportRef.current) { const scrollableContentHeight = viewportRef.current.scrollHeight; const viewportHeight = viewportRef.current.clientHeight; const scrollableContentWidth = viewportRef.current.scrollWidth; const viewportWidth = viewportRef.current.clientWidth; if (thumbYRef.current && scrollbarYRef.current && currentOrientationRef.current === 'vertical') { const scrollbarYOffset = getOffset(scrollbarYRef.current, 'padding', 'y'); const thumbYOffset = getOffset(thumbYRef.current, 'margin', 'y'); const thumbHeight = thumbYRef.current.offsetHeight; const maxThumbOffsetY = scrollbarYRef.current.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset; const scrollRatioY = deltaY / maxThumbOffsetY; viewportRef.current.scrollTop = startScrollTopRef.current + scrollRatioY * (scrollableContentHeight - viewportHeight); event.preventDefault(); setScrolling(true); window.clearTimeout(timeoutRef.current); timeoutRef.current = window.setTimeout(() => { setScrolling(false); }, SCROLL_TIMEOUT); } if (thumbXRef.current && scrollbarXRef.current && currentOrientationRef.current === 'horizontal') { const scrollbarXOffset = getOffset(scrollbarXRef.current, 'padding', 'x'); const thumbXOffset = getOffset(thumbXRef.current, 'margin', 'x'); const thumbWidth = thumbXRef.current.offsetWidth; const maxThumbOffsetX = scrollbarXRef.current.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset; const scrollRatioX = deltaX / maxThumbOffsetX; viewportRef.current.scrollLeft = startScrollLeftRef.current + scrollRatioX * (scrollableContentWidth - viewportWidth); event.preventDefault(); setScrolling(true); window.clearTimeout(timeoutRef.current); timeoutRef.current = window.setTimeout(() => { setScrolling(false); }, SCROLL_TIMEOUT); } } }); const handlePointerUp = useEventCallback(event => { thumbDraggingRef.current = false; if (thumbYRef.current && currentOrientationRef.current === 'vertical') { thumbYRef.current.releasePointerCapture(event.pointerId); } if (thumbXRef.current && currentOrientationRef.current === 'horizontal') { thumbXRef.current.releasePointerCapture(event.pointerId); } }); const handlePointerEnterOrMove = useEventCallback(({ pointerType }) => { const isTouch = pointerType === 'touch'; setTouchModality(isTouch); if (!isTouch) { setHovering(true); } }); const getRootProps = React.useCallback((externalProps = {}) => mergeReactProps(externalProps, { dir, onPointerEnter: handlePointerEnterOrMove, onPointerMove: handlePointerEnterOrMove, onPointerDown({ pointerType }) { setTouchModality(pointerType === 'touch'); }, onPointerLeave() { setHovering(false); }, style: { position: 'relative', [ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.height}px`, [ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.width}px` } }), [cornerSize, dir, handlePointerEnterOrMove]); return React.useMemo(() => ({ getRootProps, handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, setCornerSize, thumbSize, setThumbSize, touchModality, cornerRef, scrolling, setScrolling, hovering, setHovering, viewportRef, scrollbarYRef, scrollbarXRef, thumbYRef, thumbXRef, rootId, hiddenState, setHiddenState }), [getRootProps, handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, thumbSize, touchModality, cornerRef, scrolling, hovering, setHovering, viewportRef, scrollbarYRef, scrollbarXRef, thumbYRef, thumbXRef, rootId, hiddenState]); }