@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.
410 lines (405 loc) • 15.5 kB
JavaScript
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SliderControl = void 0;
var React = _interopRequireWildcard(require("react"));
var _dom = require("@floating-ui/utils/dom");
var _owner = require("@base-ui-components/utils/owner");
var _useAnimationFrame = require("@base-ui-components/utils/useAnimationFrame");
var _useStableCallback = require("@base-ui-components/utils/useStableCallback");
var _useValueAsRef = require("@base-ui-components/utils/useValueAsRef");
var _utils = require("../../floating-ui-react/utils");
var _clamp = require("../../utils/clamp");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
var _reasons = require("../../utils/reasons");
var _useRenderElement = require("../../utils/useRenderElement");
var _DirectionContext = require("../../direction-provider/DirectionContext");
var _SliderRootContext = require("../root/SliderRootContext");
var _stateAttributesMapping = require("../root/stateAttributesMapping");
var _getMidpoint = require("../utils/getMidpoint");
var _roundValueToStep = require("../utils/roundValueToStep");
var _validateMinimumDistance = require("../utils/validateMinimumDistance");
var _resolveThumbCollision = require("../utils/resolveThumbCollision");
const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
function getControlOffset(styles, vertical) {
if (!styles) {
return {
start: 0,
end: 0
};
}
function parseSize(value) {
const parsed = value != null ? parseFloat(value) : 0;
return Number.isNaN(parsed) ? 0 : parsed;
}
const start = !vertical ? 'InlineStart' : 'Top';
const end = !vertical ? 'InlineEnd' : 'Bottom';
return {
start: parseSize(styles[`border${start}Width`]) + parseSize(styles[`padding${start}`]),
end: parseSize(styles[`border${end}Width`]) + parseSize(styles[`padding${end}`])
};
}
function getFingerCoords(event, touchIdRef) {
// The event is TouchEvent
if (touchIdRef.current != null && event.changedTouches) {
const touchEvent = event;
for (let i = 0; i < touchEvent.changedTouches.length; i += 1) {
const touch = touchEvent.changedTouches[i];
if (touch.identifier === touchIdRef.current) {
return {
x: touch.clientX,
y: touch.clientY
};
}
}
return null;
}
// The event is PointerEvent
return {
x: event.clientX,
y: event.clientY
};
}
/**
* The clickable, interactive part of the slider.
* Renders a `<div>` element.
*
* Documentation: [Base UI Slider](https://base-ui.com/react/components/slider)
*/
const SliderControl = exports.SliderControl = /*#__PURE__*/React.forwardRef(function SliderControl(componentProps, forwardedRef) {
const {
render: renderProp,
className,
...elementProps
} = componentProps;
const {
disabled,
dragging,
validation,
inset,
lastChangedValueRef,
lastChangeReasonRef,
max,
min,
minStepsBetweenValues,
onValueCommitted,
orientation,
pressedInputRef,
pressedThumbCenterOffsetRef,
pressedThumbIndexRef,
pressedValuesRef,
registerFieldControlRef,
renderBeforeHydration,
setActive,
setDragging,
setValue,
state,
step,
thumbCollisionBehavior,
thumbRefs,
values
} = (0, _SliderRootContext.useSliderRootContext)();
const direction = (0, _DirectionContext.useDirection)();
const range = values.length > 1;
const vertical = orientation === 'vertical';
const controlRef = React.useRef(null);
const stylesRef = React.useRef(null);
const setStylesRef = (0, _useStableCallback.useStableCallback)(element => {
if (element && stylesRef.current == null) {
if (stylesRef.current == null) {
stylesRef.current = getComputedStyle(element);
}
}
});
// A number that uniquely identifies the current finger in the touch session.
const touchIdRef = React.useRef(null);
// The number of touch/pointermove events that have fired.
const moveCountRef = React.useRef(0);
// The offset amount to each side of the control for inset sliders.
// This value should be equal to the radius or half the width/height of the thumb.
const insetThumbOffsetRef = React.useRef(0);
const latestValuesRef = (0, _useValueAsRef.useValueAsRef)(values);
const updatePressedThumb = (0, _useStableCallback.useStableCallback)(nextIndex => {
if (pressedThumbIndexRef.current !== nextIndex) {
pressedThumbIndexRef.current = nextIndex;
}
const thumbElement = thumbRefs.current[nextIndex];
if (!thumbElement) {
pressedThumbCenterOffsetRef.current = null;
pressedInputRef.current = null;
return;
}
pressedInputRef.current = thumbElement.querySelector('input[type="range"]');
});
const getFingerState = (0, _useStableCallback.useStableCallback)(fingerCoords => {
const control = controlRef.current;
if (!control) {
return null;
}
const {
width,
height,
bottom,
left,
right
} = control.getBoundingClientRect();
const controlOffset = getControlOffset(stylesRef.current, vertical);
const insetThumbOffset = insetThumbOffsetRef.current;
const controlSize = (vertical ? height : width) - controlOffset.start - controlOffset.end - insetThumbOffset * 2;
const thumbCenterOffset = pressedThumbCenterOffsetRef.current ?? 0;
const fingerX = fingerCoords.x - thumbCenterOffset;
const fingerY = fingerCoords.y - thumbCenterOffset;
const valueSize = vertical ? bottom - fingerY - controlOffset.end : (direction === 'rtl' ? right - fingerX : fingerX - left) - controlOffset.start;
// the value at the finger origin scaled down to fit the range [0, 1]
const valueRescaled = (0, _clamp.clamp)((valueSize - insetThumbOffset) / controlSize, 0, 1);
let newValue = (max - min) * valueRescaled + min;
newValue = (0, _roundValueToStep.roundValueToStep)(newValue, step, min);
newValue = (0, _clamp.clamp)(newValue, min, max);
if (!range) {
return {
value: newValue,
thumbIndex: 0,
didSwap: false
};
}
const thumbIndex = pressedThumbIndexRef.current;
if (thumbIndex < 0) {
return null;
}
const collisionResult = (0, _resolveThumbCollision.resolveThumbCollision)({
behavior: thumbCollisionBehavior,
values,
currentValues: latestValuesRef.current ?? values,
initialValues: pressedValuesRef.current,
pressedIndex: thumbIndex,
nextValue: newValue,
min,
max,
step,
minStepsBetweenValues
});
if (thumbCollisionBehavior === 'swap' && collisionResult.didSwap) {
updatePressedThumb(collisionResult.thumbIndex);
} else {
pressedThumbIndexRef.current = collisionResult.thumbIndex;
}
return collisionResult;
});
const startPressing = (0, _useStableCallback.useStableCallback)(fingerCoords => {
pressedValuesRef.current = range ? values.slice() : null;
latestValuesRef.current = values;
const pressedThumbIndex = pressedThumbIndexRef.current;
let closestThumbIndex = pressedThumbIndex;
if (pressedThumbIndex > -1 && pressedThumbIndex < values.length) {
if (values[pressedThumbIndex] === max) {
let candidateIndex = pressedThumbIndex;
while (candidateIndex > 0 && values[candidateIndex - 1] === max) {
candidateIndex -= 1;
}
closestThumbIndex = candidateIndex;
}
} else {
// pressed on control
const axis = !vertical ? 'x' : 'y';
let minDistance;
closestThumbIndex = -1;
for (let i = 0; i < thumbRefs.current.length; i += 1) {
const thumbEl = thumbRefs.current[i];
if ((0, _dom.isElement)(thumbEl)) {
const midpoint = (0, _getMidpoint.getMidpoint)(thumbEl);
const distance = Math.abs(fingerCoords[axis] - midpoint[axis]);
if (minDistance === undefined || distance <= minDistance) {
closestThumbIndex = i;
minDistance = distance;
}
}
}
}
if (closestThumbIndex > -1 && closestThumbIndex !== pressedThumbIndex) {
updatePressedThumb(closestThumbIndex);
}
if (inset) {
const thumbEl = thumbRefs.current[closestThumbIndex];
if ((0, _dom.isElement)(thumbEl)) {
const thumbRect = thumbEl.getBoundingClientRect();
const side = !vertical ? 'width' : 'height';
insetThumbOffsetRef.current = thumbRect[side] / 2;
}
}
});
const focusThumb = (0, _useStableCallback.useStableCallback)(thumbIndex => {
thumbRefs.current?.[thumbIndex]?.querySelector('input[type="range"]')?.focus({
preventScroll: true
});
});
const handleTouchMove = (0, _useStableCallback.useStableCallback)(nativeEvent => {
const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
if (fingerCoords == null) {
return;
}
moveCountRef.current += 1;
// Cancel move in case some other element consumed a pointerup event and it was not fired.
if (nativeEvent.type === 'pointermove' && nativeEvent.buttons === 0) {
handleTouchEnd(nativeEvent);
return;
}
const finger = getFingerState(fingerCoords);
if (finger == null) {
return;
}
if ((0, _validateMinimumDistance.validateMinimumDistance)(finger.value, step, minStepsBetweenValues)) {
if (!dragging && moveCountRef.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) {
setDragging(true);
}
setValue(finger.value, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.drag, nativeEvent, undefined, {
activeThumbIndex: finger.thumbIndex
}));
latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
if (finger.didSwap) {
focusThumb(finger.thumbIndex);
}
}
});
function handleTouchEnd(nativeEvent) {
setActive(-1);
setDragging(false);
pressedInputRef.current = null;
pressedThumbCenterOffsetRef.current = null;
pressedThumbIndexRef.current = -1;
const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
const finger = fingerCoords != null ? getFingerState(fingerCoords) : null;
if (finger != null) {
const commitReason = lastChangeReasonRef.current;
validation.commit(lastChangedValueRef.current ?? finger.value);
onValueCommitted(lastChangedValueRef.current ?? finger.value, (0, _createBaseUIEventDetails.createGenericEventDetails)(commitReason, nativeEvent));
}
if ('pointerType' in nativeEvent && controlRef.current?.hasPointerCapture(nativeEvent.pointerId)) {
controlRef.current?.releasePointerCapture(nativeEvent.pointerId);
}
touchIdRef.current = null;
pressedValuesRef.current = null;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stopListening();
}
const handleTouchStart = (0, _useStableCallback.useStableCallback)(nativeEvent => {
if (disabled) {
return;
}
const touch = nativeEvent.changedTouches[0];
if (touch != null) {
touchIdRef.current = touch.identifier;
}
const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
if (fingerCoords != null) {
startPressing(fingerCoords);
const finger = getFingerState(fingerCoords);
if (finger == null) {
return;
}
focusThumb(finger.thumbIndex);
setValue(finger.value, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.trackPress, nativeEvent, undefined, {
activeThumbIndex: finger.thumbIndex
}));
latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
if (finger.didSwap) {
focusThumb(finger.thumbIndex);
}
}
moveCountRef.current = 0;
const doc = (0, _owner.ownerDocument)(controlRef.current);
doc.addEventListener('touchmove', handleTouchMove, {
passive: true
});
doc.addEventListener('touchend', handleTouchEnd, {
passive: true
});
});
const stopListening = (0, _useStableCallback.useStableCallback)(() => {
const doc = (0, _owner.ownerDocument)(controlRef.current);
doc.removeEventListener('pointermove', handleTouchMove);
doc.removeEventListener('pointerup', handleTouchEnd);
doc.removeEventListener('touchmove', handleTouchMove);
doc.removeEventListener('touchend', handleTouchEnd);
pressedValuesRef.current = null;
});
const focusFrame = (0, _useAnimationFrame.useAnimationFrame)();
React.useEffect(() => {
const control = controlRef.current;
if (!control) {
return () => stopListening();
}
control.addEventListener('touchstart', handleTouchStart, {
passive: true
});
return () => {
control.removeEventListener('touchstart', handleTouchStart);
focusFrame.cancel();
stopListening();
};
}, [stopListening, handleTouchStart, controlRef, focusFrame]);
React.useEffect(() => {
if (disabled) {
stopListening();
}
}, [disabled, stopListening]);
const element = (0, _useRenderElement.useRenderElement)('div', componentProps, {
state,
ref: [forwardedRef, registerFieldControlRef, controlRef, setStylesRef],
props: [{
['data-base-ui-slider-control']: renderBeforeHydration ? '' : undefined,
onPointerDown(event) {
const control = controlRef.current;
if (!control || disabled || event.defaultPrevented || !(0, _dom.isElement)(event.target) ||
// Only handle left clicks
event.button !== 0) {
return;
}
const fingerCoords = getFingerCoords(event, touchIdRef);
if (fingerCoords != null) {
startPressing(fingerCoords);
const finger = getFingerState(fingerCoords);
if (finger == null) {
return;
}
const pressedOnFocusedThumb = (0, _utils.contains)(thumbRefs.current[finger.thumbIndex], (0, _utils.activeElement)((0, _owner.ownerDocument)(control)));
if (pressedOnFocusedThumb) {
event.preventDefault();
} else {
focusFrame.request(() => {
focusThumb(finger.thumbIndex);
});
}
setDragging(true);
const pressedOnAnyThumb = pressedThumbCenterOffsetRef.current != null;
if (!pressedOnAnyThumb) {
setValue(finger.value, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.trackPress, event.nativeEvent, undefined, {
activeThumbIndex: finger.thumbIndex
}));
latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
if (finger.didSwap) {
focusThumb(finger.thumbIndex);
}
}
}
if (event.nativeEvent.pointerId) {
control.setPointerCapture(event.nativeEvent.pointerId);
}
moveCountRef.current = 0;
const doc = (0, _owner.ownerDocument)(controlRef.current);
doc.addEventListener('pointermove', handleTouchMove, {
passive: true
});
doc.addEventListener('pointerup', handleTouchEnd, {
once: true
});
},
tabIndex: -1
}, elementProps],
stateAttributesMapping: _stateAttributesMapping.sliderStateAttributesMapping
});
return element;
});
if (process.env.NODE_ENV !== "production") SliderControl.displayName = "SliderControl";