@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.
304 lines (299 loc) • 11.9 kB
JavaScript
;
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SliderRoot = void 0;
var React = _interopRequireWildcard(require("react"));
var _owner = require("@base-ui-components/utils/owner");
var _useControlled = require("@base-ui-components/utils/useControlled");
var _useStableCallback = require("@base-ui-components/utils/useStableCallback");
var _useValueAsRef = require("@base-ui-components/utils/useValueAsRef");
var _useIsoLayoutEffect = require("@base-ui-components/utils/useIsoLayoutEffect");
var _warn = require("@base-ui-components/utils/warn");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
var _useValueChanged = require("../../utils/useValueChanged");
var _useBaseUiId = require("../../utils/useBaseUiId");
var _useRenderElement = require("../../utils/useRenderElement");
var _clamp = require("../../utils/clamp");
var _areArraysEqual = require("../../utils/areArraysEqual");
var _utils = require("../../floating-ui-react/utils");
var _CompositeList = require("../../composite/list/CompositeList");
var _useField = require("../../field/useField");
var _FieldRootContext = require("../../field/root/FieldRootContext");
var _FormContext = require("../../form/FormContext");
var _LabelableContext = require("../../labelable-provider/LabelableContext");
var _asc = require("../utils/asc");
var _getSliderValue = require("../utils/getSliderValue");
var _validateMinimumDistance = require("../utils/validateMinimumDistance");
var _stateAttributesMapping = require("./stateAttributesMapping");
var _SliderRootContext = require("./SliderRootContext");
var _reasons = require("../../utils/reasons");
var _jsxRuntime = require("react/jsx-runtime");
function getSliderChangeEventReason(event) {
return 'key' in event ? _reasons.REASONS.keyboard : _reasons.REASONS.inputChange;
}
function areValuesEqual(newValue, oldValue) {
if (typeof newValue === 'number' && typeof oldValue === 'number') {
return newValue === oldValue;
}
if (Array.isArray(newValue) && Array.isArray(oldValue)) {
return (0, _areArraysEqual.areArraysEqual)(newValue, oldValue);
}
return false;
}
/**
* Groups all parts of the slider.
* Renders a `<div>` element.
*
* Documentation: [Base UI Slider](https://base-ui.com/react/components/slider)
*/
const SliderRoot = exports.SliderRoot = /*#__PURE__*/React.forwardRef(function SliderRoot(componentProps, forwardedRef) {
const {
'aria-labelledby': ariaLabelledByProp,
className,
defaultValue,
disabled: disabledProp = false,
id: idProp,
format,
largeStep = 10,
locale,
render,
max = 100,
min = 0,
minStepsBetweenValues = 0,
name: nameProp,
onValueChange: onValueChangeProp,
onValueCommitted: onValueCommittedProp,
orientation = 'horizontal',
step = 1,
thumbCollisionBehavior = 'push',
thumbAlignment = 'center',
value: valueProp,
...elementProps
} = componentProps;
const id = (0, _useBaseUiId.useBaseUiId)(idProp);
const onValueChange = (0, _useStableCallback.useStableCallback)(onValueChangeProp);
const onValueCommitted = (0, _useStableCallback.useStableCallback)(onValueCommittedProp);
const {
clearErrors
} = (0, _FormContext.useFormContext)();
const {
state: fieldState,
disabled: fieldDisabled,
name: fieldName,
setTouched,
setDirty,
validityData,
shouldValidateOnChange,
validation
} = (0, _FieldRootContext.useFieldRootContext)();
const {
labelId
} = (0, _LabelableContext.useLabelableContext)();
const ariaLabelledby = ariaLabelledByProp ?? labelId;
const disabled = fieldDisabled || disabledProp;
const name = fieldName ?? nameProp;
// The internal value is potentially unsorted, e.g. to support frozen arrays
// https://github.com/mui/material-ui/pull/28472
const [valueUnwrapped, setValueUnwrapped] = (0, _useControlled.useControlled)({
controlled: valueProp,
default: defaultValue ?? min,
name: 'Slider'
});
const sliderRef = React.useRef(null);
const controlRef = React.useRef(null);
const thumbRefs = React.useRef([]);
// The input element nested in the pressed thumb.
const pressedInputRef = React.useRef(null);
// The px distance between the pointer and the center of a pressed thumb.
const pressedThumbCenterOffsetRef = React.useRef(null);
// The index of the pressed thumb, or the closest thumb if the `Control` was pressed.
// This is updated on pointerdown, which is sooner than the `active/activeIndex`
// state which is updated later when the nested `input` receives focus.
const pressedThumbIndexRef = React.useRef(-1);
// The values when the current drag interaction started.
const pressedValuesRef = React.useRef(null);
const lastChangedValueRef = React.useRef(null);
const lastChangeReasonRef = React.useRef('none');
const formatOptionsRef = (0, _useValueAsRef.useValueAsRef)(format);
// 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, setActiveState] = React.useState(-1);
const [lastUsedThumbIndex, setLastUsedThumbIndex] = React.useState(-1);
const [dragging, setDragging] = React.useState(false);
const [thumbMap, setThumbMap] = React.useState(() => new Map());
const [indicatorPosition, setIndicatorPosition] = React.useState([undefined, undefined]);
const setActive = (0, _useStableCallback.useStableCallback)(value => {
setActiveState(value);
if (value !== -1) {
setLastUsedThumbIndex(value);
}
});
(0, _useField.useField)({
id,
commit: validation.commit,
value: valueUnwrapped,
controlRef,
name,
getValue: () => valueUnwrapped
});
(0, _useValueChanged.useValueChanged)(valueUnwrapped, () => {
clearErrors(name);
if (shouldValidateOnChange()) {
validation.commit(valueUnwrapped);
} else {
validation.commit(valueUnwrapped, true);
}
const initialValue = validityData.initialValue;
let isDirty;
if (Array.isArray(valueUnwrapped) && Array.isArray(initialValue)) {
isDirty = !(0, _areArraysEqual.areArraysEqual)(valueUnwrapped, initialValue);
} else {
isDirty = valueUnwrapped !== initialValue;
}
setDirty(isDirty);
});
const registerFieldControlRef = (0, _useStableCallback.useStableCallback)(element => {
if (element) {
controlRef.current = element;
}
});
const range = Array.isArray(valueUnwrapped);
const values = React.useMemo(() => {
if (!range) {
return [(0, _clamp.clamp)(valueUnwrapped, min, max)];
}
return valueUnwrapped.slice().sort(_asc.asc);
}, [max, min, range, valueUnwrapped]);
const setValue = (0, _useStableCallback.useStableCallback)((newValue, details) => {
if (Number.isNaN(newValue) || areValuesEqual(newValue, valueUnwrapped)) {
return;
}
const changeDetails = details ?? (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.none, undefined, undefined, {
activeThumbIndex: -1
});
lastChangeReasonRef.current = changeDetails.reason;
// 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.
// @ts-expect-error The nativeEvent is function, not object
const clonedEvent = new event.constructor(event.type, event);
Object.defineProperty(clonedEvent, 'target', {
writable: true,
value: {
value: newValue,
name
}
});
changeDetails.event = clonedEvent;
lastChangedValueRef.current = newValue;
onValueChange(newValue, changeDetails);
if (changeDetails.isCanceled) {
return;
}
setValueUnwrapped(newValue);
});
const handleInputChange = (0, _useStableCallback.useStableCallback)((valueInput, index, event) => {
const newValue = (0, _getSliderValue.getSliderValue)(valueInput, index, min, max, range, values);
if ((0, _validateMinimumDistance.validateMinimumDistance)(newValue, step, minStepsBetweenValues)) {
const reason = getSliderChangeEventReason(event);
setValue(newValue, (0, _createBaseUIEventDetails.createChangeEventDetails)(reason, event.nativeEvent, undefined, {
activeThumbIndex: index
}));
setTouched(true);
const nextValue = lastChangedValueRef.current ?? newValue;
onValueCommitted(nextValue, (0, _createBaseUIEventDetails.createGenericEventDetails)(reason, event.nativeEvent));
}
});
if (process.env.NODE_ENV !== 'production') {
if (min >= max) {
(0, _warn.warn)('Slider `max` must be greater than `min`.');
}
}
(0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => {
const activeEl = (0, _utils.activeElement)((0, _owner.ownerDocument)(sliderRef.current));
if (disabled && activeEl && 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
activeEl.blur();
}
}, [disabled]);
if (disabled && active !== -1) {
setActive(-1);
}
const state = React.useMemo(() => ({
...fieldState,
activeThumbIndex: active,
disabled,
dragging,
orientation,
max,
min,
minStepsBetweenValues,
step,
values
}), [fieldState, active, disabled, dragging, max, min, minStepsBetweenValues, orientation, step, values]);
const contextValue = React.useMemo(() => ({
active,
controlRef,
disabled,
dragging,
validation,
formatOptionsRef,
handleInputChange,
indicatorPosition,
inset: thumbAlignment !== 'center',
labelId: ariaLabelledby,
largeStep,
lastUsedThumbIndex,
lastChangedValueRef,
lastChangeReasonRef,
locale,
max,
min,
minStepsBetweenValues,
name,
onValueCommitted,
orientation,
pressedInputRef,
pressedThumbCenterOffsetRef,
pressedThumbIndexRef,
pressedValuesRef,
registerFieldControlRef,
renderBeforeHydration: thumbAlignment === 'edge',
setActive,
setDragging,
setIndicatorPosition,
setValue,
state,
step,
thumbCollisionBehavior,
thumbMap,
thumbRefs,
values
}), [active, controlRef, ariaLabelledby, disabled, dragging, validation, formatOptionsRef, handleInputChange, indicatorPosition, largeStep, lastUsedThumbIndex, lastChangedValueRef, lastChangeReasonRef, locale, max, min, minStepsBetweenValues, name, onValueCommitted, orientation, pressedInputRef, pressedThumbCenterOffsetRef, pressedThumbIndexRef, pressedValuesRef, registerFieldControlRef, setActive, setDragging, setIndicatorPosition, setValue, state, step, thumbCollisionBehavior, thumbAlignment, thumbMap, thumbRefs, values]);
const element = (0, _useRenderElement.useRenderElement)('div', componentProps, {
state,
ref: [forwardedRef, sliderRef],
props: [{
'aria-labelledby': ariaLabelledby,
id,
role: 'group'
}, validation.getValidationProps, elementProps],
stateAttributesMapping: _stateAttributesMapping.sliderStateAttributesMapping
});
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_SliderRootContext.SliderRootContext.Provider, {
value: contextValue,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_CompositeList.CompositeList, {
elementsRef: thumbRefs,
onMapChange: setThumbMap,
children: element
})
});
});
if (process.env.NODE_ENV !== "production") SliderRoot.displayName = "SliderRoot";