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