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