@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.
362 lines (354 loc) • 12.5 kB
JavaScript
;
'use client';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.focusThumb = focusThumb;
exports.trackFinger = trackFinger;
exports.useSliderRoot = useSliderRoot;
exports.validateMinimumDistance = validateMinimumDistance;
var React = _interopRequireWildcard(require("react"));
var _utils = require("@floating-ui/react/utils");
var _areArraysEqual = require("../../utils/areArraysEqual");
var _clamp = require("../../utils/clamp");
var _mergeReactProps = require("../../utils/mergeReactProps");
var _owner = require("../../utils/owner");
var _useControlled = require("../../utils/useControlled");
var _useEnhancedEffect = require("../../utils/useEnhancedEffect");
var _useForkRef = require("../../utils/useForkRef");
var _useBaseUiId = require("../../utils/useBaseUiId");
var _valueToPercent = require("../../utils/valueToPercent");
var _useField = require("../../field/useField");
var _FieldRootContext = require("../../field/root/FieldRootContext");
var _useFieldControlValidation = require("../../field/control/useFieldControlValidation");
var _utils2 = require("../utils");
var _asc = require("../utils/asc");
var _setValueIndex = require("../utils/setValueIndex");
var _getSliderValue = require("../utils/getSliderValue");
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; }
function findClosest(values, currentValue) {
const {
index: closestIndex
} = values.reduce((acc, value, index) => {
const distance = Math.abs(currentValue - value);
if (acc === null || distance < acc.distance || distance === acc.distance) {
return {
distance,
index
};
}
return acc;
}, null) ?? {};
return closestIndex;
}
function focusThumb({
sliderRef,
activeIndex,
setActive
}) {
const doc = (0, _owner.ownerDocument)(sliderRef.current);
if (!sliderRef.current?.contains(doc.activeElement) || Number(doc?.activeElement?.getAttribute('data-index')) !== activeIndex) {
sliderRef.current?.querySelector(`[type="range"][data-index="${activeIndex}"]`).focus();
}
if (setActive) {
setActive(activeIndex);
}
}
function validateMinimumDistance(values, step, minStepsBetweenValues) {
if (!Array.isArray(values)) {
return true;
}
const distances = values.reduce((acc, val, index, vals) => {
if (index === vals.length - 1) {
return acc;
}
acc.push(Math.abs(val - vals[index + 1]));
return acc;
}, []);
return Math.min(...distances) >= step * minStepsBetweenValues;
}
function trackFinger(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 false;
}
// The event is PointerEvent
return {
x: event.clientX,
y: event.clientY
};
}
/**
*/
function useSliderRoot(parameters) {
const {
'aria-labelledby': ariaLabelledby,
defaultValue,
direction = 'ltr',
disabled = false,
id: idProp,
largeStep = 10,
max = 100,
min = 0,
minStepsBetweenValues = 0,
name,
onValueChange,
onValueCommitted,
orientation = 'horizontal',
rootRef,
step = 1,
tabIndex,
value: valueProp
} = parameters;
const {
setControlId,
setTouched,
setDirty,
validityData
} = (0, _FieldRootContext.useFieldRootContext)();
const {
getValidationProps,
inputRef: inputValidationRef,
commitValidation
} = (0, _useFieldControlValidation.useFieldControlValidation)();
const [valueState, setValueState] = (0, _useControlled.useControlled)({
controlled: valueProp,
default: defaultValue ?? min,
name: 'Slider'
});
const sliderRef = React.useRef(null);
const controlRef = React.useRef(null);
const thumbRefs = React.useRef([]);
const id = (0, _useBaseUiId.useBaseUiId)(idProp);
(0, _useEnhancedEffect.useEnhancedEffect)(() => {
setControlId(id);
return () => {
setControlId(undefined);
};
}, [id, setControlId]);
(0, _useField.useField)({
id,
commitValidation,
value: valueState,
controlRef
});
// We can't use the :active browser pseudo-classes.
// - The active state isn't triggered when clicking on the rail.
// - The active state isn't transferred when inversing a range slider.
const [active, setActive] = React.useState(-1);
const [dragging, setDragging] = React.useState(false);
const registerSliderControl = React.useCallback(element => {
if (element) {
controlRef.current = element;
inputValidationRef.current = element.querySelector('input[type="range"]');
}
}, [inputValidationRef]);
// Map with index (DOM position) as the key and the id attribute of each thumb <input> element as the value
const [inputIdMap, setInputMap] = React.useState(() => new Map());
const deregisterInputId = React.useCallback(index => {
setInputMap(prevMap => {
const nextMap = new Map(prevMap);
nextMap.delete(index);
return nextMap;
});
}, []);
const registerInputId = React.useCallback((index, inputId) => {
if (index > -1 && inputId !== undefined) {
setInputMap(prevMap => new Map(prevMap).set(index, inputId));
}
return {
deregister: deregisterInputId
};
}, [deregisterInputId]);
const handleValueChange = React.useCallback((value, thumbIndex, event) => {
if (!onValueChange) {
return;
}
// Redefine target to allow name and value to be read.
// This allows seamless integration with the most popular form libraries.
// https://github.com/mui/material-ui/issues/13485#issuecomment-676048492
// Clone the event to not override `target` of the original event.
const nativeEvent = event.nativeEvent || event;
// @ts-ignore The nativeEvent is function, not object
const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent);
Object.defineProperty(clonedEvent, 'target', {
writable: true,
value: {
value,
name
}
});
onValueChange(value, clonedEvent, thumbIndex);
}, [name, onValueChange]);
const range = Array.isArray(valueState);
const values = React.useMemo(() => {
return (range ? valueState.slice().sort(_asc.asc) : [valueState]).map(val => val == null ? min : (0, _clamp.clamp)(val, min, max));
}, [max, min, range, valueState]);
const handleRootRef = (0, _useForkRef.useForkRef)(rootRef, sliderRef);
const areValuesEqual = React.useCallback(newValue => {
if (typeof newValue === 'number' && typeof valueState === 'number') {
return newValue === valueState;
}
if (typeof newValue === 'object' && typeof valueState === 'object') {
return (0, _areArraysEqual.areArraysEqual)(newValue, valueState);
}
return false;
}, [valueState]);
const changeValue = React.useCallback((valueInput, index, event) => {
const newValue = (0, _getSliderValue.getSliderValue)({
valueInput,
min,
max,
index,
range,
values
});
if (range) {
focusThumb({
sliderRef,
activeIndex: index
});
}
if (validateMinimumDistance(newValue, step, minStepsBetweenValues)) {
setValueState(newValue);
setDirty(newValue !== validityData.initialValue);
if (handleValueChange && !areValuesEqual(newValue) && event) {
handleValueChange(newValue, index, event);
}
setTouched(true);
commitValidation(newValue);
if (onValueCommitted && event) {
onValueCommitted(newValue, event.nativeEvent);
}
}
}, [min, max, range, step, minStepsBetweenValues, values, setValueState, setDirty, validityData.initialValue, handleValueChange, areValuesEqual, onValueCommitted, setTouched, commitValidation]);
const previousIndexRef = React.useRef(null);
const getFingerNewValue = React.useCallback(({
finger,
move = false,
offset = 0
}) => {
const {
current: sliderControl
} = controlRef;
if (!sliderControl) {
return null;
}
const isRtl = direction === 'rtl';
const isVertical = orientation === 'vertical';
const {
width,
height,
bottom,
left
} = sliderControl.getBoundingClientRect();
let percent;
if (isVertical) {
percent = (bottom - finger.y) / height + offset;
} else {
percent = (finger.x - left) / width + offset * (isRtl ? -1 : 1);
}
percent = Math.min(percent, 1);
if (isRtl && !isVertical) {
percent = 1 - percent;
}
let newValue;
newValue = (0, _utils2.percentToValue)(percent, min, max);
if (step) {
newValue = (0, _utils2.roundValueToStep)(newValue, step, min);
}
newValue = (0, _clamp.clamp)(newValue, min, max);
let activeIndex = 0;
if (!range) {
return {
newValue,
activeIndex,
newPercentageValue: percent
};
}
if (!move) {
activeIndex = findClosest(values, newValue);
} else {
activeIndex = previousIndexRef.current;
}
// Bound the new value to the thumb's neighbours.
newValue = (0, _clamp.clamp)(newValue, values[activeIndex - 1] + minStepsBetweenValues || -Infinity, values[activeIndex + 1] - minStepsBetweenValues || Infinity);
const previousValue = newValue;
newValue = (0, _setValueIndex.setValueIndex)({
values,
newValue,
index: activeIndex
});
// Potentially swap the index if needed.
if (!move) {
activeIndex = newValue.indexOf(previousValue);
previousIndexRef.current = activeIndex;
}
return {
newValue,
activeIndex,
newPercentageValue: percent
};
}, [direction, max, min, minStepsBetweenValues, orientation, range, step, values]);
(0, _useEnhancedEffect.useEnhancedEffect)(() => {
const activeEl = (0, _utils.activeElement)((0, _owner.ownerDocument)(sliderRef.current));
if (disabled && sliderRef.current.contains(activeEl)) {
// This is necessary because Firefox and Safari will keep focus
// on a disabled element:
// https://codesandbox.io/p/sandbox/mui-pr-22247-forked-h151h?file=/src/App.js
// @ts-ignore
activeEl.blur();
}
}, [disabled]);
if (disabled && active !== -1) {
setActive(-1);
}
const getRootProps = React.useCallback((externalProps = {}) => (0, _mergeReactProps.mergeReactProps)(getValidationProps(externalProps), {
'aria-labelledby': ariaLabelledby,
id,
ref: handleRootRef,
role: 'group'
}), [ariaLabelledby, getValidationProps, handleRootRef, id]);
return React.useMemo(() => ({
getRootProps,
active,
areValuesEqual,
'aria-labelledby': ariaLabelledby,
changeValue,
direction,
disabled,
dragging,
getFingerNewValue,
handleValueChange,
inputIdMap,
largeStep,
max,
min,
minStepsBetweenValues,
name,
onValueCommitted,
orientation,
percentageValues: values.map(v => (0, _valueToPercent.valueToPercent)(v, min, max)),
range,
registerInputId,
registerSliderControl,
setActive,
setDragging,
setValueState,
step,
tabIndex,
thumbRefs,
values
}), [getRootProps, active, areValuesEqual, ariaLabelledby, changeValue, direction, disabled, dragging, getFingerNewValue, handleValueChange, inputIdMap, largeStep, max, min, minStepsBetweenValues, name, onValueCommitted, orientation, range, registerInputId, registerSliderControl, setActive, setDragging, setValueState, step, tabIndex, thumbRefs, values]);
}