@julo-ui/sliders
Version:
A React Slider component that implements input[type='range']
461 lines (458 loc) • 14.1 kB
JavaScript
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
};