@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
JavaScript
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]);
}