UNPKG

@julo-ui/sliders

Version:

A React Slider component that implements input[type='range']

461 lines (458 loc) 14.1 kB
import { use_handle_focus_thumb_default } from "./chunk-WUIKB4RN.mjs"; import { use_handle_pan_event_default } from "./chunk-TINVUJCQ.mjs"; import { getIds, getThumbStateOnChange, getValueBounds } from "./chunk-XX7IAI35.mjs"; import { use_handle_dragging_default } from "./chunk-ZAZRIR7U.mjs"; import { use_handle_focus_default } from "./chunk-CRNUCIZN.mjs"; import { use_handle_reversed_default } from "./chunk-J3CHJ35F.mjs"; import { use_handle_style_default } from "./chunk-PHTCSOKW.mjs"; import { roundValueToStep, valueToPercent } from "./chunk-Y2VS5NAR.mjs"; import { RANGE_SLIDER_DEFAULT_VALUE } from "./chunk-26ITYHRB.mjs"; // src/range-slider/use-range-slider.ts import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { useCallbackRef } from "@julo-ui/use-callback-ref"; import { clampValue } from "@julo-ui/number-utils"; import { useControllableState } from "@julo-ui/use-controllable-state"; import { useWatchElementsSize } from "@julo-ui/use-watch-element-size"; import { ariaAttr, dataAttr, mergeRefs } from "@julo-ui/dom-utils"; import { callAllFn, isTrullyEmpty } from "@julo-ui/function-utils"; function useRangeSlider(props) { const { min = 0, max = 100, onChange, value: valueProp, defaultValue, isReversed: isReversedProp, direction = "ltr", orientation = "horizontal", id: idProp, isDisabled = false, isReadOnly, onChangeStart: onChangeStartProp, onChangeEnd: onChangeEndProp, step = 1, getAriaValueText: getAriaValueTextProp, "aria-valuetext": ariaValueText, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, name, focusThumbOnChange = true, minStepsBetweenThumbs = 0, isDisableSwap = false, ...resRootProps } = props; const onChangeStart = useCallbackRef(onChangeStartProp); const onChangeEnd = useCallbackRef(onChangeEndProp); const getAriaValueText = useCallbackRef(getAriaValueTextProp); const isReversed = use_handle_reversed_default({ isReversed: isReversedProp, direction, orientation }); const [computedValue, setComputedValue] = useControllableState({ value: valueProp, defaultValue: defaultValue != null ? defaultValue : RANGE_SLIDER_DEFAULT_VALUE, onChange }); if (!Array.isArray(computedValue)) { throw new TypeError( `[range-slider] You passed an invalid value for \`value\` or \`defaultValue\`, expected \`Array\` but got \`${typeof computedValue}\`` ); } const [activeIndex, setActiveIndex] = useState(-1); const initialValue = useRef(computedValue); const isInteractive = !(isDisabled || isReadOnly); const tenSteps = (max - min) / 10; const oneStep = step || (max - min) / 100; const value = computedValue.map((val) => clampValue(val, min, max)); const prevValue = useRef(value); const distanceBetweenThumbs = minStepsBetweenThumbs * step; const valueBounds = getValueBounds(value, min, max, distanceBetweenThumbs); const reversedValue = value.map((val) => max - val + min); const thumbValues = isReversed ? reversedValue : value; const thumbPercents = thumbValues.map((val) => valueToPercent(val, min, max)); const isVertical = orientation === "vertical"; const trackRef = useRef(null); const rootRef = useRef(null); const reactId = useId(); const uuid = idProp != null ? idProp : reactId; const ids = getIds(uuid); const thumbSizes = useWatchElementsSize({ getNodes() { const rootNode = rootRef.current; const thumbNodes = rootNode == null ? void 0 : rootNode.querySelectorAll("[role=slider]"); return thumbNodes ? Array.from(thumbNodes) : []; } }); const { isDragging, onDraggingStart, onDraggingEnd } = use_handle_dragging_default(); const { isFocused, onBlur: onInputBlur, onFocus: onInputFocus } = use_handle_focus_default(); const sliderStates = useMemo( () => ({ eventSource: null, value, valueBounds, getThumbMaxValue: (index) => valueBounds[index].max, getThumbMinValue: (index) => valueBounds[index].min, getThumbPercent: (index) => thumbPercents[index], isFocused, isDragging, focusThumbOnChange, isDisabled, isInteractive, isReversed, isVertical, max, min, orientation, step: oneStep, tenSteps, distanceBetweenThumbs }), [ distanceBetweenThumbs, focusThumbOnChange, isDisabled, isDragging, isFocused, isInteractive, isReversed, isVertical, max, min, oneStep, orientation, tenSteps, thumbPercents, value, valueBounds ] ); const { onFocusThumb } = use_handle_focus_thumb_default({ activeIndex, focusThumbOnChange, ids, rootRef }); const actions = useMemo( () => ({ setValueAtIndex(index, value2) { if (!sliderStates.isInteractive) return; const bounds = sliderStates.valueBounds[index]; value2 = parseFloat(roundValueToStep(value2, bounds.min, oneStep)); value2 = clampValue(value2, bounds.min, bounds.max); const next = [...sliderStates.value]; next[index] = value2; setComputedValue(next); }, setActiveIndex, stepUp(index, step2 = oneStep) { const bounds = [...sliderStates.valueBounds]; const values = [...sliderStates.value]; const valueAtIndex = sliderStates.value[index]; const next = sliderStates.isReversed ? valueAtIndex - step2 : valueAtIndex + step2; const { index: newIndex, value: value2 } = getThumbStateOnChange({ bounds, values, pointerValue: next, index, isDisableSwap, prevValue: prevValue.current, step: sliderStates.step, distanceBetweenThumbs }); actions.setValue(value2); setActiveIndex(newIndex); onFocusThumb(newIndex); }, stepDown(index, step2 = oneStep) { const bounds = [...sliderStates.valueBounds]; const values = [...sliderStates.value]; const valueAtIndex = sliderStates.value[index]; const next = sliderStates.isReversed ? valueAtIndex + step2 : valueAtIndex - step2; const { index: newIndex, value: value2 } = getThumbStateOnChange({ bounds, values, pointerValue: next, index, isDisableSwap, prevValue: prevValue.current, step: sliderStates.step, distanceBetweenThumbs }); actions.setValue(value2); setActiveIndex(newIndex); onFocusThumb(newIndex); }, setValue: setComputedValue, reset() { setComputedValue(initialValue.current); } }), [ distanceBetweenThumbs, isDisableSwap, onFocusThumb, oneStep, setComputedValue, sliderStates.isInteractive, sliderStates.isReversed, sliderStates.step, sliderStates.value, sliderStates.valueBounds ] ); const { getThumbStyle, innerTrackStyle, getMarkerStyle, rootStyle, trackStyle } = use_handle_style_default({ isReversed: sliderStates.isReversed, orientation: sliderStates.orientation, thumbPercents, thumbSizes }); use_handle_pan_event_default({ onChangeEnd, onChangeStart, onDraggingEnd, onDraggingStart, onFocusThumb, rootRef, sliderStates, activeIndex, trackRef, actions, isDisableSwap, prevValue, distanceBetweenThumbs }); const onThumbKeyUp = useCallback(() => { prevValue.current = sliderStates.value; }, [sliderStates.value]); const onThumbKeyDown = useCallback( (event) => { const keyMap = { ArrowRight: () => actions.stepUp(activeIndex), ArrowUp: () => actions.stepUp(activeIndex), ArrowLeft: () => actions.stepDown(activeIndex), ArrowDown: () => actions.stepDown(activeIndex), PageUp: () => actions.stepUp(activeIndex, tenSteps), PageDown: () => actions.stepDown(activeIndex, tenSteps), Home: () => { const { min: value2 } = valueBounds[activeIndex]; actions.setValueAtIndex(activeIndex, value2); }, End: () => { const { max: value2 } = valueBounds[activeIndex]; actions.setValueAtIndex(activeIndex, value2); } }; const action = keyMap[event.key]; if (action) { event.preventDefault(); event.stopPropagation(); action(event); sliderStates.eventSource = "keyboard"; } }, [actions, activeIndex, sliderStates, tenSteps, valueBounds] ); const getRootProps = useCallback( (props2 = {}, forwardedRef = null) => ({ ...resRootProps, ...props2, id: ids.root, ref: mergeRefs(forwardedRef, rootRef), tabIndex: -1, style: { ...props2.style, ...rootStyle }, "aria-disabled": ariaAttr(isDisabled), "data-focused": dataAttr(isFocused) }), [ids.root, isDisabled, isFocused, resRootProps, rootStyle] ); const getTrackProps = useCallback( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: mergeRefs(forwardedRef, trackRef), id: ids.track, "data-disabled": dataAttr(isDisabled), style: { ...props2.style, ...trackStyle } }), [ids.track, isDisabled, trackStyle] ); const getInnerTrackProps = useCallback( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: forwardedRef, id: ids.innerTrack, style: { ...props2.style, ...innerTrackStyle } }), [ids.innerTrack, innerTrackStyle] ); const getThumbProps = useCallback( (props2, forwardedRef = null) => { var _a; const { index, style, onKeyDown, onKeyUp, onFocus, onBlur, ...resProps } = props2; const valueAtIndex = sliderStates.value[index]; const bounds = sliderStates.valueBounds[index]; if (isTrullyEmpty(valueAtIndex)) throw new TypeError( `[range-slider > thumb] Cannot find value at index \`${index}\`. The \`value\` or \`defaultValue\` length is : ${sliderStates.value.length}` ); return { ...resProps, ref: forwardedRef, role: "slider", ...isInteractive && { tabIndex: 0 }, id: ids.getThumb(index), "data-active": dataAttr(isDragging && activeIndex === index), "aria-valuetext": (_a = getAriaValueText == null ? void 0 : getAriaValueText(valueAtIndex)) != null ? _a : ariaValueText == null ? void 0 : ariaValueText[index], "aria-valuemin": bounds.min, "aria-valuemax": bounds.max, "aria-valuenow": valueAtIndex, "aria-orientation": orientation, "aria-disabled": ariaAttr(isDisabled), "aria-readonly": ariaAttr(isReadOnly), "aria-label": ariaLabel == null ? void 0 : ariaLabel[index], ...!(ariaLabel == null ? void 0 : ariaLabel[index]) && { "aria-labelledby": ariaLabelledBy == null ? void 0 : ariaLabelledBy[index] }, style: { ...style, ...getThumbStyle(index) }, onKeyUp: callAllFn(onThumbKeyUp, onKeyUp), onKeyDown: callAllFn(onThumbKeyDown, onKeyDown), onFocus: callAllFn(onInputFocus, () => setActiveIndex(index), onFocus), onBlur: callAllFn(onInputBlur, () => setActiveIndex(-1), onBlur) }; }, [ activeIndex, ariaLabel, ariaLabelledBy, ariaValueText, getAriaValueText, getThumbStyle, ids, isDisabled, isDragging, isInteractive, isReadOnly, onInputBlur, onInputFocus, onThumbKeyDown, onThumbKeyUp, orientation, sliderStates.value, sliderStates.valueBounds ] ); const getOutputProps = useCallback( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: forwardedRef, id: ids.output, htmlFor: value.map((_v, i) => ids.getThumb(i)).join(" "), "aria-live": "off" }), [ids, value] ); const getMarkerProps = useCallback( (props2, forwardedRef = null) => { const { value: markerValue, style, ...resProps } = props2; const isInRange = !(markerValue < sliderStates.min || markerValue || sliderStates.max); const isHighlighted = markerValue >= sliderStates.value[0] && markerValue <= sliderStates.value[sliderStates.value.length - 1]; const markerPercent = valueToPercent(markerValue, min, max); const percent = isReversed ? 100 - markerPercent : markerPercent; return { ...resProps, ref: forwardedRef, role: "presentation", "aria-hidden": true, "data-disabled": dataAttr(isDisabled), "data-invalid": dataAttr(!isInRange), "data-highlighted": dataAttr(isHighlighted), style: { ...style, ...getMarkerStyle(percent) } }; }, [ getMarkerStyle, isDisabled, isReversed, max, min, sliderStates.max, sliderStates.min, sliderStates.value ] ); const getInputProps = useCallback( (props2, forwardedRef = null) => { const { index, ...resProps } = props2; return { ...resProps, ref: forwardedRef, id: ids.getInput(index), type: "hidden", value: sliderStates.value[index], name: name ? Array.isArray(name) ? name[index] : `${name}-${index}` : `${ids.getInput(index)}-${index}` }; }, [ids, name, sliderStates.value] ); useEffect(() => { if (sliderStates.eventSource === "keyboard") { onChangeEnd == null ? void 0 : onChangeEnd(sliderStates.value); } }, [value, onChangeEnd, sliderStates.eventSource, sliderStates.value]); return { state: sliderStates, actions, getRootProps, getTrackProps, getInnerTrackProps, getThumbProps, getMarkerProps, getInputProps, getOutputProps }; } export { useRangeSlider };