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.

233 lines (229 loc) 9.23 kB
"use strict"; 'use client'; Object.defineProperty(exports, "__esModule", { value: true }); exports.useScrub = useScrub; var React = _interopRequireWildcard(require("react")); var ReactDOM = _interopRequireWildcard(require("react-dom")); var _constants = require("../utils/constants"); var _getViewportRect = require("../utils/getViewportRect"); var _subscribeToVisualViewportResize = require("../utils/subscribeToVisualViewportResize"); var _owner = require("../../utils/owner"); var _useLatestRef = require("../../utils/useLatestRef"); var _detectBrowser = require("../../utils/detectBrowser"); var _mergeReactProps = require("../../utils/mergeReactProps"); var _NumberFieldRootDataAttributes = require("./NumberFieldRootDataAttributes"); 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; } /** * @ignore - internal hook. */ function useScrub(params) { const { disabled, readOnly, value, inputRef, incrementValue, getStepAmount } = params; const latestValueRef = (0, _useLatestRef.useLatestRef)(value); const scrubHandleRef = React.useRef(null); const scrubAreaRef = React.useRef(null); const avoidFlickerTimeoutRef = React.useRef(-1); const isScrubbingRef = React.useRef(false); const scrubAreaCursorRef = React.useRef(null); const virtualCursorCoords = React.useRef({ x: 0, y: 0 }); const visualScaleRef = React.useRef(1); const [isScrubbing, setIsScrubbing] = React.useState(false); const [cursorTransform, setCursorTransform] = React.useState(''); React.useEffect(() => { return () => { clearTimeout(avoidFlickerTimeoutRef.current); }; }, []); React.useEffect(() => { if (!isScrubbing || !scrubAreaCursorRef.current) { return undefined; } return (0, _subscribeToVisualViewportResize.subscribeToVisualViewportResize)(scrubAreaCursorRef.current, visualScaleRef); }, [isScrubbing]); const onScrub = React.useCallback(({ movementX, movementY }) => { const virtualCursor = scrubAreaCursorRef.current; const scrubAreaEl = scrubAreaRef.current; const scrubHandle = scrubHandleRef.current; if (!virtualCursor || !scrubAreaEl || !scrubHandle) { return; } const rect = (0, _getViewportRect.getViewportRect)(scrubHandle.teleportDistance, scrubAreaEl); const coords = virtualCursorCoords.current; const newCoords = { x: Math.round(coords.x + movementX), y: Math.round(coords.y + movementY) }; const cursorWidth = virtualCursor.offsetWidth; const cursorHeight = virtualCursor.offsetHeight; if (newCoords.x + cursorWidth / 2 < rect.x) { newCoords.x = rect.width - cursorWidth / 2; } else if (newCoords.x + cursorWidth / 2 > rect.width) { newCoords.x = rect.x - cursorWidth / 2; } if (newCoords.y + cursorHeight / 2 < rect.y) { newCoords.y = rect.height - cursorHeight / 2; } else if (newCoords.y + cursorHeight / 2 > rect.height) { newCoords.y = rect.y - cursorHeight / 2; } virtualCursorCoords.current = newCoords; setCursorTransform(`translate3d(${newCoords.x}px,${newCoords.y}px,0) scale(${1 / visualScaleRef.current})`); }, []); const onScrubbingChange = React.useCallback((scrubbingValue, { clientX, clientY }) => { ReactDOM.flushSync(() => { setIsScrubbing(scrubbingValue); }); const virtualCursor = scrubAreaCursorRef.current; if (!virtualCursor || !scrubbingValue) { return; } const initialCoords = { x: clientX - virtualCursor.offsetWidth / 2, y: clientY - virtualCursor.offsetHeight / 2 }; virtualCursorCoords.current = initialCoords; setCursorTransform(`translate3d(${initialCoords.x}px,${initialCoords.y}px,0) scale(${1 / visualScaleRef.current})`); }, []); const getScrubAreaProps = React.useCallback((externalProps = {}) => (0, _mergeReactProps.mergeReactProps)(externalProps, { role: 'presentation', [_NumberFieldRootDataAttributes.NumberFieldRootDataAttributes.scrubbing]: isScrubbing || undefined, style: { touchAction: 'none', WebkitUserSelect: 'none', userSelect: 'none' }, onPointerDown(event) { const isMainButton = !event.button || event.button === 0; if (event.defaultPrevented || readOnly || !isMainButton || disabled) { return; } if (event.pointerType === 'mouse') { event.preventDefault(); inputRef.current?.focus(); } isScrubbingRef.current = true; onScrubbingChange(true, event.nativeEvent); // WebKit causes significant layout shift with the native message, so we can't use it. if (!(0, _detectBrowser.isWebKit)()) { // There can be some frames where there's no cursor at all when requesting the pointer lock. // This is a workaround to avoid flickering. avoidFlickerTimeoutRef.current = window.setTimeout(async () => { try { // Avoid non-deterministic errors in testing environments. This error sometimes // appears: // "The root document of this element is not valid for pointer lock." // We need to await it even though it doesn't appear to return a promise in the // types in order for the `catch` to work. await (0, _owner.ownerDocument)(scrubAreaRef.current).body.requestPointerLock(); } catch { // } }, 20); } } }), [readOnly, disabled, onScrubbingChange, inputRef, isScrubbing]); const getScrubAreaCursorProps = React.useCallback((externalProps = {}) => (0, _mergeReactProps.mergeReactProps)({ ...externalProps, style: { ...externalProps.style, transform: `${cursorTransform} ${externalProps.style?.transform || ''}`.trim() } }, { role: 'presentation', style: { position: 'fixed', top: 0, left: 0, pointerEvents: 'none' } }), [cursorTransform]); React.useEffect(function registerGlobalScrubbingEventListeners() { if (!inputRef.current || disabled || readOnly) { return undefined; } let cumulativeDelta = 0; function handleScrubPointerUp(event) { clearTimeout(avoidFlickerTimeoutRef.current); isScrubbingRef.current = false; onScrubbingChange(false, event); if (!(0, _detectBrowser.isWebKit)()) { try { // Avoid errors in testing environments. (0, _owner.ownerDocument)(scrubAreaRef.current).exitPointerLock(); } catch { // } } } function handleScrubPointerMove(event) { if (!isScrubbingRef.current || !scrubHandleRef.current) { return; } // Prevent text selection. event.preventDefault(); onScrub(event); const { direction, pixelSensitivity } = scrubHandleRef.current; const { movementX, movementY } = event; cumulativeDelta += direction === 'vertical' ? movementY : movementX; if (Math.abs(cumulativeDelta) >= pixelSensitivity) { cumulativeDelta = 0; const dValue = direction === 'vertical' ? -movementY : movementX; incrementValue(dValue * (getStepAmount() ?? _constants.DEFAULT_STEP), 1); } } const win = (0, _owner.ownerWindow)(inputRef.current); win.addEventListener('pointerup', handleScrubPointerUp, true); win.addEventListener('pointermove', handleScrubPointerMove, true); return () => { win.removeEventListener('pointerup', handleScrubPointerUp, true); win.removeEventListener('pointermove', handleScrubPointerMove, true); }; }, [disabled, readOnly, incrementValue, latestValueRef, getStepAmount, inputRef, onScrubbingChange, onScrub]); // Prevent scrolling using touch input when scrubbing. React.useEffect(function registerScrubberTouchPreventListener() { const element = scrubAreaRef.current; if (!element || disabled || readOnly) { return undefined; } function handleTouchStart(event) { if (event.touches.length === 1) { event.preventDefault(); } } element.addEventListener('touchstart', handleTouchStart); return () => { element.removeEventListener('touchstart', handleTouchStart); }; }, [disabled, readOnly]); return React.useMemo(() => ({ isScrubbing, getScrubAreaProps, getScrubAreaCursorProps, scrubAreaCursorRef, scrubAreaRef, scrubHandleRef }), [isScrubbing, getScrubAreaProps, getScrubAreaCursorProps]); }