@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.
258 lines (254 loc) • 9.81 kB
JavaScript
;
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.NumberFieldScrubArea = void 0;
var React = _interopRequireWildcard(require("react"));
var ReactDOM = _interopRequireWildcard(require("react-dom"));
var _owner = require("@base-ui-components/utils/owner");
var _detectBrowser = require("@base-ui-components/utils/detectBrowser");
var _useValueAsRef = require("@base-ui-components/utils/useValueAsRef");
var _useStableCallback = require("@base-ui-components/utils/useStableCallback");
var _NumberFieldRootContext = require("../root/NumberFieldRootContext");
var _stateAttributesMapping = require("../utils/stateAttributesMapping");
var _NumberFieldScrubAreaContext = require("./NumberFieldScrubAreaContext");
var _useRenderElement = require("../../utils/useRenderElement");
var _getViewportRect = require("../utils/getViewportRect");
var _subscribeToVisualViewportResize = require("../utils/subscribeToVisualViewportResize");
var _constants = require("../utils/constants");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
var _reasons = require("../../utils/reasons");
var _jsxRuntime = require("react/jsx-runtime");
/**
* An interactive area where the user can click and drag to change the field value.
* Renders a `<span>` element.
*
* Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
*/
const NumberFieldScrubArea = exports.NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldScrubArea(componentProps, forwardedRef) {
const {
render,
className,
direction = 'horizontal',
pixelSensitivity = 2,
teleportDistance,
...elementProps
} = componentProps;
const {
state
} = (0, _NumberFieldRootContext.useNumberFieldRootContext)();
const {
isScrubbing,
setIsScrubbing,
disabled,
readOnly,
value,
inputRef,
incrementValue,
getStepAmount,
onValueCommitted,
lastChangedValueRef,
valueRef
} = (0, _NumberFieldRootContext.useNumberFieldRootContext)();
const latestValueRef = (0, _useValueAsRef.useValueAsRef)(value);
const scrubAreaRef = React.useRef(null);
const isScrubbingRef = React.useRef(false);
const scrubAreaCursorRef = React.useRef(null);
const virtualCursorCoords = React.useRef({
x: 0,
y: 0
});
const visualScaleRef = React.useRef(1);
const [isTouchInput, setIsTouchInput] = React.useState(false);
const [isPointerLockDenied, setIsPointerLockDenied] = React.useState(false);
React.useEffect(() => {
if (!isScrubbing || !scrubAreaCursorRef.current) {
return undefined;
}
return (0, _subscribeToVisualViewportResize.subscribeToVisualViewportResize)(scrubAreaCursorRef.current, visualScaleRef);
}, [isScrubbing]);
function updateCursorTransform(x, y) {
if (scrubAreaCursorRef.current) {
scrubAreaCursorRef.current.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / visualScaleRef.current})`;
}
}
const onScrub = (0, _useStableCallback.useStableCallback)(({
movementX,
movementY
}) => {
const virtualCursor = scrubAreaCursorRef.current;
const scrubAreaEl = scrubAreaRef.current;
if (!virtualCursor || !scrubAreaEl) {
return;
}
const rect = (0, _getViewportRect.getViewportRect)(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;
updateCursorTransform(newCoords.x, newCoords.y);
});
const onScrubbingChange = (0, _useStableCallback.useStableCallback)((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;
updateCursorTransform(initialCoords.x, initialCoords.y);
});
React.useEffect(function registerGlobalScrubbingEventListeners() {
// Only listen while actively scrubbing; avoids unrelated pointerup events committing.
if (!inputRef.current || disabled || readOnly || !isScrubbing) {
return undefined;
}
let cumulativeDelta = 0;
function handleScrubPointerUp(event) {
try {
// Avoid errors in testing environments.
(0, _owner.ownerDocument)(scrubAreaRef.current).exitPointerLock();
} catch {
//
} finally {
isScrubbingRef.current = false;
onScrubbingChange(false, event);
onValueCommitted(lastChangedValueRef.current ?? valueRef.current, (0, _createBaseUIEventDetails.createGenericEventDetails)(_reasons.REASONS.scrub, event));
}
}
function handleScrubPointerMove(event) {
if (!isScrubbingRef.current) {
return;
}
// Prevent text selection.
event.preventDefault();
onScrub(event);
const {
movementX,
movementY
} = event;
cumulativeDelta += direction === 'vertical' ? movementY : movementX;
if (Math.abs(cumulativeDelta) >= pixelSensitivity) {
cumulativeDelta = 0;
const dValue = direction === 'vertical' ? -movementY : movementX;
const stepAmount = getStepAmount(event) ?? _constants.DEFAULT_STEP;
const rawAmount = dValue * stepAmount;
if (rawAmount !== 0) {
incrementValue(Math.abs(rawAmount), {
direction: rawAmount >= 0 ? 1 : -1,
event,
reason: _reasons.REASONS.scrub
});
}
}
}
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, isScrubbing, incrementValue, latestValueRef, getStepAmount, inputRef, onScrubbingChange, onScrub, direction, pixelSensitivity, lastChangedValueRef, onValueCommitted, valueRef]);
// 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]);
const defaultProps = {
role: 'presentation',
style: {
touchAction: 'none',
WebkitUserSelect: 'none',
userSelect: 'none'
},
async onPointerDown(event) {
const isMainButton = !event.button || event.button === 0;
if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
return;
}
const isTouch = event.pointerType === 'touch';
setIsTouchInput(isTouch);
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 (!isTouch && !_detectBrowser.isWebKit) {
try {
// Avoid non-deterministic errors in testing environments. This error sometimes
// appears:
// "The root document of this element is not valid for pointer lock."
await (0, _owner.ownerDocument)(scrubAreaRef.current).body.requestPointerLock();
setIsPointerLockDenied(false);
} catch (error) {
setIsPointerLockDenied(true);
} finally {
if (isScrubbingRef.current) {
ReactDOM.flushSync(() => {
onScrubbingChange(true, event.nativeEvent);
});
}
}
}
}
};
const element = (0, _useRenderElement.useRenderElement)('span', componentProps, {
ref: [forwardedRef, scrubAreaRef],
state,
props: [defaultProps, elementProps],
stateAttributesMapping: _stateAttributesMapping.stateAttributesMapping
});
const contextValue = React.useMemo(() => ({
isScrubbing,
isTouchInput,
isPointerLockDenied,
scrubAreaCursorRef,
scrubAreaRef,
direction,
pixelSensitivity,
teleportDistance
}), [isScrubbing, isTouchInput, isPointerLockDenied, direction, pixelSensitivity, teleportDistance]);
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NumberFieldScrubAreaContext.NumberFieldScrubAreaContext.Provider, {
value: contextValue,
children: element
});
});
if (process.env.NODE_ENV !== "production") NumberFieldScrubArea.displayName = "NumberFieldScrubArea";