@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.
341 lines (335 loc) • 12.9 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 _owner = require("@base-ui-components/utils/owner");
var _useAnimationFrame = require("@base-ui-components/utils/useAnimationFrame");
var _useEventCallback = require("@base-ui-components/utils/useEventCallback");
var _utils = require("../../floating-ui-react/utils");
var _clamp = require("../../utils/clamp");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
var _useRenderElement = require("../../utils/useRenderElement");
var _valueToPercent = require("../../utils/valueToPercent");
var _DirectionContext = require("../../direction-provider/DirectionContext");
var _SliderRootContext = require("../root/SliderRootContext");
var _styleHooks = require("../root/styleHooks");
var _replaceArrayItemAtIndex = require("../utils/replaceArrayItemAtIndex");
var _roundValueToStep = require("../utils/roundValueToStep");
var _validateMinimumDistance = require("../utils/validateMinimumDistance");
const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
function getClosestThumbIndex(values, currentValue, max) {
let closestIndex;
let minDistance;
for (let i = 0; i < values.length; i += 1) {
const distance = Math.abs(currentValue - values[i]);
if (minDistance === undefined || (
// when the value is at max, the lowest index thumb has to be dragged
// first or it will block higher index thumbs from moving
// otherwise consider higher index thumbs to be closest when their values are identical
values[i] === max ? distance < minDistance : distance <= minDistance)) {
closestIndex = i;
minDistance = distance;
}
}
return closestIndex;
}
function getControlOffset(styles, orientation) {
if (!styles) {
return {
start: 0,
end: 0
};
}
const start = orientation === 'horizontal' ? 'InlineStart' : 'Top';
const end = orientation === 'horizontal' ? 'InlineEnd' : 'Bottom';
return {
start: parseFloat(styles[`border${start}Width`]) + parseFloat(styles[`padding${start}`]),
end: parseFloat(styles[`border${end}Width`]) + parseFloat(styles[`padding${end}`])
};
}
function getFingerPosition(event, touchIdRef) {
// The event is TouchEvent
if (touchIdRef.current !== undefined && 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,
fieldControlValidation,
pressedInputRef,
lastChangedValueRef,
max,
min,
minStepsBetweenValues,
onValueCommitted,
orientation,
range,
registerFieldControlRef,
setActive,
setDragging,
setValue,
state,
step,
thumbRefs,
values
} = (0, _SliderRootContext.useSliderRootContext)();
const controlRef = React.useRef(null);
const stylesRef = React.useRef(null);
const setStylesRef = (0, _useEventCallback.useEventCallback)(element => {
if (element && stylesRef.current == null) {
if (stylesRef.current == null) {
stylesRef.current = getComputedStyle(element);
}
}
});
const closestThumbIndexRef = React.useRef(null);
// A number that uniquely identifies the current finger in the touch session.
const touchIdRef = React.useRef(null);
const moveCountRef = React.useRef(0);
/**
* The difference between the value at the finger origin and the value at
* the center of the thumb scaled down to fit the range [0, 1]
*/
const offsetRef = React.useRef(0);
const direction = (0, _DirectionContext.useDirection)();
const getFingerState = (0, _useEventCallback.useEventCallback)((fingerPosition,
/**
* When `true`, closestThumbIndexRef is updated.
* It's `true` when called by touchstart or pointerdown.
*/
shouldCaptureThumbIndex = false,
/**
* The difference between the value at the finger origin and the value at
* the center of the thumb scaled down to fit the range [0, 1]
*/
thumbOffset = 0) => {
if (fingerPosition == null) {
return null;
}
const control = controlRef.current;
if (!control) {
return null;
}
const isRtl = direction === 'rtl';
const isVertical = orientation === 'vertical';
const {
width,
height,
bottom,
left,
right
} = control.getBoundingClientRect();
const controlOffset = getControlOffset(stylesRef.current, orientation);
// the value at the finger origin scaled down to fit the range [0, 1]
let valueRescaled = isVertical ? (bottom - controlOffset.end - fingerPosition.y) / (height - controlOffset.start - controlOffset.end) + thumbOffset : (isRtl ? right - controlOffset.start - fingerPosition.x : fingerPosition.x - left - controlOffset.start) / (width - controlOffset.start - controlOffset.end) + thumbOffset * (isRtl ? -1 : 1);
valueRescaled = (0, _clamp.clamp)(valueRescaled, 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,
valueRescaled,
thumbIndex: 0
};
}
if (shouldCaptureThumbIndex) {
closestThumbIndexRef.current = getClosestThumbIndex(values, newValue, max) ?? 0;
}
const closestThumbIndex = closestThumbIndexRef.current ?? 0;
const minValueDifference = minStepsBetweenValues * step;
// Bound the new value to the thumb's neighbours.
newValue = (0, _clamp.clamp)(newValue, values[closestThumbIndex - 1] + minValueDifference || -Infinity, values[closestThumbIndex + 1] - minValueDifference || Infinity);
return {
value: (0, _replaceArrayItemAtIndex.replaceArrayItemAtIndex)(values, closestThumbIndex, newValue),
valueRescaled,
thumbIndex: closestThumbIndex
};
});
const focusThumb = (0, _useEventCallback.useEventCallback)(thumbIndex => {
thumbRefs.current?.[thumbIndex]?.querySelector('input[type="range"]')?.focus({
preventScroll: true
});
});
const handleTouchMove = (0, _useEventCallback.useEventCallback)(nativeEvent => {
const fingerPosition = getFingerPosition(nativeEvent, touchIdRef);
if (fingerPosition == 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) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
handleTouchEnd(nativeEvent);
return;
}
const finger = getFingerState(fingerPosition, false, offsetRef.current);
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, finger.thumbIndex, nativeEvent);
}
});
const handleTouchEnd = (0, _useEventCallback.useEventCallback)(nativeEvent => {
const fingerPosition = getFingerPosition(nativeEvent, touchIdRef);
setDragging(false);
pressedInputRef.current = null;
if (fingerPosition == null) {
return;
}
const finger = getFingerState(fingerPosition, false);
if (finger == null) {
return;
}
setActive(-1);
fieldControlValidation.commitValidation(lastChangedValueRef.current ?? finger.value);
onValueCommitted(lastChangedValueRef.current ?? finger.value, (0, _createBaseUIEventDetails.createBaseUIEventDetails)('none', nativeEvent));
if ('pointerType' in nativeEvent && controlRef.current?.hasPointerCapture(nativeEvent.pointerId)) {
controlRef.current?.releasePointerCapture(nativeEvent.pointerId);
}
touchIdRef.current = null;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stopListening();
});
const handleTouchStart = (0, _useEventCallback.useEventCallback)(nativeEvent => {
if (disabled) {
return;
}
const touch = nativeEvent.changedTouches[0];
if (touch != null) {
touchIdRef.current = touch.identifier;
}
const fingerPosition = getFingerPosition(nativeEvent, touchIdRef);
if (fingerPosition != null) {
const finger = getFingerState(fingerPosition, true);
if (finger == null) {
return;
}
focusThumb(finger.thumbIndex);
setValue(finger.value, finger.thumbIndex, nativeEvent);
}
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, _useEventCallback.useEventCallback)(() => {
offsetRef.current = 0;
const doc = (0, _owner.ownerDocument)(controlRef.current);
doc.removeEventListener('pointermove', handleTouchMove);
doc.removeEventListener('pointerup', handleTouchEnd);
doc.removeEventListener('touchmove', handleTouchMove);
doc.removeEventListener('touchend', handleTouchEnd);
});
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: [{
onPointerDown(event) {
const control = controlRef.current;
if (!control || disabled || event.defaultPrevented ||
// Only handle left clicks
event.button !== 0) {
return;
}
const fingerPosition = getFingerPosition(event, touchIdRef);
if (fingerPosition != null) {
const finger = getFingerState(fingerPosition, true);
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);
// if the event lands on a thumb, don't change the value, just get the
// percentageValue difference represented by the distance between the click origin
// and the coordinates of the value on the track area
if (thumbRefs.current.includes(event.target)) {
offsetRef.current = (0, _valueToPercent.valueToPercent)(values[finger.thumbIndex], min, max) / 100 - finger.valueRescaled;
} else {
setValue(finger.value, finger.thumbIndex, event.nativeEvent);
}
}
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);
},
tabIndex: -1
}, elementProps],
customStyleHookMapping: _styleHooks.sliderStyleHookMapping
});
return element;
});
if (process.env.NODE_ENV !== "production") SliderControl.displayName = "SliderControl";