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.

175 lines (170 loc) 7.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useScrollAreaViewport = useScrollAreaViewport; var React = _interopRequireWildcard(require("react")); var _ScrollAreaRootContext = require("../root/ScrollAreaRootContext"); var _useEventCallback = require("../../utils/useEventCallback"); var _useEnhancedEffect = require("../../utils/useEnhancedEffect"); var _mergeReactProps = require("../../utils/mergeReactProps"); var _clamp = require("../../utils/clamp"); var _constants = require("../constants"); var _getOffset = require("../utils/getOffset"); var _jsxRuntime = require("react/jsx-runtime"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function useScrollAreaViewport(params) { const { children } = params; const { viewportRef, scrollbarYRef, scrollbarXRef, thumbYRef, thumbXRef, cornerRef, dir, setCornerSize, setThumbSize, rootId, setHiddenState, hiddenState, handleScroll, setHovering } = (0, _ScrollAreaRootContext.useScrollAreaRootContext)(); const contentWrapperRef = React.useRef(null); const computeThumb = (0, _useEventCallback.useEventCallback)(() => { const viewportEl = viewportRef.current; const scrollbarYEl = scrollbarYRef.current; const scrollbarXEl = scrollbarXRef.current; const thumbYEl = thumbYRef.current; const thumbXEl = thumbXRef.current; const cornerEl = cornerRef.current; if (!viewportEl) { return; } const scrollableContentHeight = viewportEl.scrollHeight; const scrollableContentWidth = viewportEl.scrollWidth; const viewportHeight = viewportEl.clientHeight; const viewportWidth = viewportEl.clientWidth; const scrollTop = viewportEl.scrollTop; const scrollLeft = viewportEl.scrollLeft; const scrollbarYHidden = viewportHeight >= scrollableContentHeight; const scrollbarXHidden = viewportWidth >= scrollableContentWidth; const nextWidth = scrollbarXHidden ? 0 : viewportWidth / scrollableContentWidth * viewportWidth; const nextHeight = scrollbarYHidden ? 0 : viewportHeight / scrollableContentHeight * viewportHeight; const scrollbarXOffset = (0, _getOffset.getOffset)(scrollbarXEl, 'padding', 'x'); const scrollbarYOffset = (0, _getOffset.getOffset)(scrollbarYEl, 'padding', 'y'); const thumbXOffset = (0, _getOffset.getOffset)(thumbXEl, 'margin', 'x'); const thumbYOffset = (0, _getOffset.getOffset)(thumbYEl, 'margin', 'y'); const clampedNextWidth = Math.max(_constants.MIN_THUMB_SIZE, nextWidth - scrollbarXOffset - thumbXOffset); const clampedNextHeight = Math.max(_constants.MIN_THUMB_SIZE, nextHeight - scrollbarYOffset - thumbYOffset); setThumbSize(prevSize => { if (prevSize.height === clampedNextHeight && prevSize.width === clampedNextWidth) { return prevSize; } return { width: clampedNextWidth, height: clampedNextHeight }; }); // Handle Y (vertical) scroll if (scrollbarYEl && thumbYEl) { const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset; const scrollRatioY = scrollTop / (scrollableContentHeight - viewportHeight); // In Safari, don't allow it to go negative or too far as `scrollTop` considers the rubber // band effect. const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY)); thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`; } // Handle X (horizontal) scroll if (scrollbarXEl && thumbXEl) { const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset; const scrollRatioX = scrollLeft / (scrollableContentWidth - viewportWidth); // In Safari, don't allow it to go negative or too far as `scrollLeft` considers the rubber // band effect. const thumbOffsetX = dir === 'rtl' ? (0, _clamp.clamp)(scrollRatioX * maxThumbOffsetX, -maxThumbOffsetX, 0) : (0, _clamp.clamp)(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX); thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`; } if (cornerEl) { if (scrollbarXHidden || scrollbarYHidden) { setCornerSize({ width: 0, height: 0 }); } else if (!scrollbarXHidden && !scrollbarYHidden) { const width = scrollbarYEl?.offsetWidth || 0; const height = scrollbarXEl?.offsetHeight || 0; setCornerSize({ width, height }); } } setHiddenState(prevState => { const cornerHidden = scrollbarYHidden || scrollbarXHidden; if (prevState.scrollbarYHidden === scrollbarYHidden && prevState.scrollbarXHidden === scrollbarXHidden && prevState.cornerHidden === cornerHidden) { return prevState; } return { scrollbarYHidden, scrollbarXHidden, cornerHidden }; }); }); (0, _useEnhancedEffect.useEnhancedEffect)(() => { // First load computation. // Wait for the scrollbar-related refs to be set. queueMicrotask(computeThumb); }, [computeThumb]); (0, _useEnhancedEffect.useEnhancedEffect)(() => { computeThumb(); }, [computeThumb, hiddenState, dir]); (0, _useEnhancedEffect.useEnhancedEffect)(() => { // `onMouseEnter` doesn't fire upon load, so we need to check if the viewport is already // being hovered. if (viewportRef.current?.matches(':hover')) { setHovering(true); } }, [viewportRef, setHovering]); React.useEffect(() => { if (!contentWrapperRef.current || !viewportRef.current || typeof ResizeObserver === 'undefined') { return undefined; } const ro = new ResizeObserver(computeThumb); ro.observe(contentWrapperRef.current); ro.observe(viewportRef.current); return () => { ro.disconnect(); }; }, [computeThumb, viewportRef]); const getViewportProps = React.useCallback((externalProps = {}) => (0, _mergeReactProps.mergeReactProps)(externalProps, { ...(rootId && { 'data-id': `${rootId}-viewport` }), // https://accessibilityinsights.io/info-examples/web/scrollable-region-focusable/ ...((!hiddenState.scrollbarXHidden || !hiddenState.scrollbarYHidden) && { tabIndex: 0 }), style: { overflow: 'scroll' }, onScroll() { computeThumb(); handleScroll(); }, children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { ref: contentWrapperRef, style: { minWidth: 'fit-content' }, children: children }) }), [rootId, hiddenState.scrollbarXHidden, hiddenState.scrollbarYHidden, children, computeThumb, handleScroll]); return React.useMemo(() => ({ getViewportProps }), [getViewportProps]); }