UNPKG

@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
"use strict"; '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";