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.

362 lines (354 loc) 12.5 kB
"use strict"; '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]); }