UNPKG

@base-ui/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.

203 lines (199 loc) 8.11 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScrollAreaScrollbar = void 0; var React = _interopRequireWildcard(require("react")); var _addEventListener = require("@base-ui/utils/addEventListener"); var _utils = require("../../floating-ui-react/utils"); var _ScrollAreaRootContext = require("../root/ScrollAreaRootContext"); var _ScrollAreaScrollbarContext = require("./ScrollAreaScrollbarContext"); var _useRenderElement = require("../../internals/useRenderElement"); var _getOffset = require("../utils/getOffset"); var _DirectionContext = require("../../internals/direction-context/DirectionContext"); var _stateAttributes = require("../root/stateAttributes"); var _jsxRuntime = require("react/jsx-runtime"); /** * A vertical or horizontal scrollbar for the scroll area. * Renders a `<div>` element. * * Documentation: [Base UI Scroll Area](https://base-ui.com/react/components/scroll-area) */ const ScrollAreaScrollbar = exports.ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScrollbar(componentProps, forwardedRef) { const { render, className, orientation = 'vertical', keepMounted = false, style, ...elementProps } = componentProps; const { hovering, scrollingX, scrollingY, hiddenState, scrollbarYRef, scrollbarXRef, viewportRef, thumbYRef, thumbXRef, handlePointerDown, handlePointerUp, handleScroll, disableViewportSnap, rootId, thumbSize, hasMeasuredScrollbar, viewportState } = (0, _ScrollAreaRootContext.useScrollAreaRootContext)(); const vertical = orientation === 'vertical'; const state = { ...viewportState, hovering, scrolling: vertical ? scrollingY : scrollingX, orientation }; const direction = (0, _DirectionContext.useDirection)(); const hideTrackUntilMeasured = !hasMeasuredScrollbar && !keepMounted; const isHidden = vertical ? hiddenState.y : hiddenState.x; const shouldRender = keepMounted || !isHidden; React.useEffect(() => { if (!shouldRender) { return undefined; } const viewportEl = viewportRef.current; const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current; if (!scrollbarEl) { return undefined; } function handleWheel(event) { if (!viewportEl || event.ctrlKey) { return; } const horizontal = !vertical; const scrollProperty = horizontal ? 'scrollLeft' : 'scrollTop'; const delta = horizontal ? event.deltaX : event.deltaY; if (delta === 0) { return; } const maxScroll = horizontal ? viewportEl.scrollWidth - viewportEl.clientWidth : viewportEl.scrollHeight - viewportEl.clientHeight; // RTL horizontal scrolling uses a negative `scrollLeft` range, from 0 to `-maxScroll`. const minScroll = horizontal && direction === 'rtl' ? -maxScroll : 0; const maxScrollValue = horizontal && direction === 'rtl' ? 0 : maxScroll; const scrollValue = viewportEl[scrollProperty]; // At an edge (or with no overflow), let the wheel event chain to the // parent/page instead of swallowing it via `preventDefault`. if (scrollValue <= minScroll && delta < 0 || scrollValue >= maxScrollValue && delta > 0) { return; } event.preventDefault(); viewportEl[scrollProperty] = Math.min(maxScrollValue, Math.max(minScroll, scrollValue + delta)); handleScroll({ x: viewportEl.scrollLeft, y: viewportEl.scrollTop }); } return (0, _addEventListener.addEventListener)(scrollbarEl, 'wheel', handleWheel, { passive: false }); }, [direction, handleScroll, vertical, scrollbarXRef, scrollbarYRef, shouldRender, viewportRef]); const props = { ...(rootId && { 'data-id': `${rootId}-scrollbar` }), onPointerDown(event) { if (event.button !== 0) { return; } const target = (0, _utils.getTarget)(event.nativeEvent); const thumbEl = vertical ? thumbYRef.current : thumbXRef.current; // Ignore clicks on thumb, including cases where React retargets the // synthetic event to the track host across a shadow boundary. if (thumbEl && (0, _utils.contains)(thumbEl, target)) { return; } const viewportEl = viewportRef.current; if (!viewportEl) { return; } const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current; if (!thumbEl || !scrollbarEl) { return; } const axis = vertical ? 'y' : 'x'; const thumbOffset = (0, _getOffset.getOffset)(thumbEl, 'margin', axis); const scrollbarOffset = (0, _getOffset.getOffset)(scrollbarEl, 'padding', axis); const thumbSizePx = vertical ? thumbEl.offsetHeight : thumbEl.offsetWidth; const trackRect = scrollbarEl.getBoundingClientRect(); const clickPosition = vertical ? event.clientY - trackRect.top - thumbSizePx / 2 - scrollbarOffset + thumbOffset / 2 : event.clientX - trackRect.left - thumbSizePx / 2 - scrollbarOffset + thumbOffset / 2; const scrollableSize = vertical ? viewportEl.scrollHeight : viewportEl.scrollWidth; const viewportSize = vertical ? viewportEl.clientHeight : viewportEl.clientWidth; const trackSize = vertical ? scrollbarEl.offsetHeight : scrollbarEl.offsetWidth; const maxThumbOffset = trackSize - thumbSizePx - scrollbarOffset - thumbOffset; // A short or heavily padded track can drive `maxThumbOffset` to zero or // negative once the thumb hits its `MIN_THUMB_SIZE` floor. Dividing by it // would yield a non-finite (`Infinity`/`NaN`) or inverted scroll position. if (maxThumbOffset <= 0) { return; } const scrollRatio = clickPosition / maxThumbOffset; const maxScrollDistance = scrollableSize - viewportSize; // Disable snapping before the jump-to-click assignment, or the // assigned position quantizes to the nearest snap point and the thumb // stays offset from the pointer for the whole drag. `handlePointerDown` // below re-runs this as a guarded no-op for the thumb-drag path. disableViewportSnap(); if (vertical) { viewportEl.scrollTop = scrollRatio * maxScrollDistance; } else if (direction === 'rtl') { viewportEl.scrollLeft = -(1 - scrollRatio) * maxScrollDistance; } else { viewportEl.scrollLeft = scrollRatio * maxScrollDistance; } handleScroll({ x: viewportEl.scrollLeft, y: viewportEl.scrollTop }); handlePointerDown(event); }, onPointerUp: handlePointerUp, // Mirror `onPointerUp` so a browser-cancelled gesture on the track (no thumb // child captures the pointer) still clears the drag state. onPointerCancel: handlePointerUp, style: { position: 'absolute', touchAction: 'none', WebkitUserSelect: 'none', userSelect: 'none', visibility: hideTrackUntilMeasured ? 'hidden' : undefined, ...(vertical ? { top: 0, bottom: 'var(--scroll-area-corner-height)', insetInlineEnd: 0, ['--scroll-area-thumb-height']: `${thumbSize.height}px` } : { insetInlineStart: 0, insetInlineEnd: 'var(--scroll-area-corner-width)', bottom: 0, ['--scroll-area-thumb-width']: `${thumbSize.width}px` }) } }; const element = (0, _useRenderElement.useRenderElement)('div', componentProps, { ref: [forwardedRef, vertical ? scrollbarYRef : scrollbarXRef], state, props: [props, elementProps], stateAttributesMapping: _stateAttributes.scrollAreaStateAttributesMapping }); if (!shouldRender) { return null; } return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ScrollAreaScrollbarContext.ScrollAreaScrollbarContext.Provider, { value: orientation, children: element }); }); if (process.env.NODE_ENV !== "production") ScrollAreaScrollbar.displayName = "ScrollAreaScrollbar";