@yamada-ui/slider
Version:
Yamada UI slider components
736 lines (734 loc) • 21.8 kB
JavaScript
"use client"
import {
getThumbSize
} from "./chunk-FXWAONIG.mjs";
// src/range-slider.tsx
import {
forwardRef,
mergeVars,
omitThemeProps,
ui,
useComponentMultiStyle
} from "@yamada-ui/core";
import {
formControlProperties,
useFormControlProps
} from "@yamada-ui/form-control";
import { useControllableState } from "@yamada-ui/use-controllable-state";
import { useLatestRef } from "@yamada-ui/use-latest-ref";
import { usePanEvent } from "@yamada-ui/use-pan-event";
import { useSizes } from "@yamada-ui/use-size";
import {
clampNumber,
createContext,
cx,
dataAttr,
findChild,
getValidChildren,
handlerAll,
includesChildren,
isArray,
isEmpty,
mergeRefs,
omitChildren,
percentToValue,
pickObject,
roundNumberToStep,
useCallbackRef,
useUpdateEffect,
valueToPercent
} from "@yamada-ui/utils";
import { useCallback, useId, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
var useRangeSlider = ({
focusThumbOnChange = true,
...props
}) => {
if (!focusThumbOnChange) props.isReadOnly = true;
const uuid = useId();
const {
id = uuid,
name = id,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledBy,
"aria-valuetext": ariaValueText,
betweenThumbs = 0,
max = 100,
min = 0,
defaultValue = [min + (max - min) / 4, max - (max - min) / 4],
getAriaValueText: getAriaValueTextProp,
isReversed,
orientation = "horizontal",
reversed = isReversed,
step = 1,
thumbSize: thumbSizeProp,
value: valueProp,
onChange,
onChangeEnd: onChangeEndProp,
onChangeStart: onChangeStartProp,
...rest
} = useFormControlProps(props);
if (max < min)
throw new Error("Do not assign a number less than 'min' to 'max'");
const {
"aria-readonly": ariaReadonly,
disabled,
readOnly,
required,
onBlur,
onFocus,
...formControlProps
} = pickObject(rest, formControlProperties);
const [computedValues, setValues] = useControllableState({
defaultValue,
value: valueProp,
onChange
});
const [dragging, setDragging] = useState(false);
const [focused, setFocused] = useState(false);
const interactive = !(disabled || readOnly);
const tenStep = (max - min) / 10;
const oneStep = step || (max - min) / 100;
const spacing = betweenThumbs * step;
const values = computedValues.map(
(value) => clampNumber(value, min, max)
);
const [startValue, endValue] = values;
const reversedValues = values.map((value) => max - value + min);
const thumbValues = reversed ? reversedValues : values;
const thumbPercents = thumbValues.map(
(value) => valueToPercent(value, min, max)
);
const valueBounds = [
{ max: endValue - spacing, min },
{ max, min: startValue + spacing }
];
const vertical = orientation === "vertical";
const latestRef = useLatestRef({
betweenThumbs,
disabled,
focusThumbOnChange,
interactive,
max,
min,
orientation,
reversed,
step,
valueBounds,
values,
vertical
});
const activeIndexRef = useRef(-1);
const eventSourceRef = useRef(null);
const containerRef = useRef(null);
const trackRef = useRef(null);
const thumbSizes = useSizes({
getNodes: () => {
var _a;
const nodes = (_a = containerRef.current) == null ? void 0 : _a.querySelectorAll("[role=slider]");
return nodes ? Array.from(nodes) : [];
}
});
const onChangeStart = useCallbackRef(onChangeStartProp);
const onChangeEnd = useCallbackRef(onChangeEndProp);
const getAriaValueText = useCallbackRef(getAriaValueTextProp);
const getThumbId = useCallback((i) => `slider-thumb-${id}-${i}`, [id]);
const getInputId = useCallback((i) => `slider-input-${id}-${i}`, [id]);
const getMarkerId = useCallback(
(i) => `slider-marker-${id}-${i}`,
[id]
);
usePanEvent(containerRef, {
onMove: (ev) => {
const activeIndex = activeIndexRef.current;
const { interactive: interactive2 } = latestRef.current;
if (!interactive2 || activeIndex == -1) return;
const pointValue = getValueFromPointer(ev) || 0;
constrain(activeIndex, pointValue);
focusThumb(activeIndex);
},
onSessionEnd: () => {
const { interactive: interactive2, values: values2 } = latestRef.current;
if (!interactive2) return;
setDragging(false);
onChangeEnd(values2);
},
onSessionStart: (ev) => {
const { interactive: interactive2, values: values2 } = latestRef.current;
if (!interactive2) return;
setDragging(true);
const pointValue = getValueFromPointer(ev) || 0;
const distances = values2.map((value) => Math.abs(value - pointValue));
const closest = Math.min(...distances);
let i = distances.indexOf(closest);
const thumbsPosition = distances.filter(
(distance) => distance === closest
);
const isThumbStacked = thumbsPosition.length > 1;
if (isThumbStacked && pointValue > values2[i])
i = i + thumbsPosition.length - 1;
activeIndexRef.current = i;
constrain(i, pointValue);
focusThumb(i);
onChangeStart(values2);
}
});
const getValueFromPointer = useCallback(
(ev) => {
var _a, _b;
if (!trackRef.current) return;
const { max: max2, min: min2 } = latestRef.current;
eventSourceRef.current = "pointer";
const { bottom, height, left, width } = trackRef.current.getBoundingClientRect();
const { clientX, clientY } = (_b = (_a = ev.touches) == null ? void 0 : _a[0]) != null ? _b : ev;
const diff = vertical ? bottom - clientY : clientX - left;
const length = vertical ? height : width;
let percent = diff / length;
if (reversed) percent = 1 - percent;
let nextValue = percentToValue(percent, min2, max2);
return nextValue;
},
[latestRef, vertical, reversed]
);
const focusThumb = useCallback(
(i) => {
var _a;
if (i === -1 || !focusThumbOnChange) return;
const id2 = getThumbId(i);
const el = (_a = containerRef.current) == null ? void 0 : _a.ownerDocument.getElementById(id2);
if (el) setTimeout(() => el.focus());
},
[focusThumbOnChange, getThumbId]
);
const constrain = useCallback(
(i, value) => {
var _a;
const { interactive: interactive2, valueBounds: valueBounds2, values: values2 } = latestRef.current;
if (!interactive2) return;
const { max: max2 = 100, min: min2 = 0 } = (_a = valueBounds2[i]) != null ? _a : {};
value = parseFloat(roundNumberToStep(value, min2, oneStep));
value = clampNumber(value, min2, max2);
const nextValues = [...values2];
nextValues[i] = value;
setValues(nextValues);
},
[latestRef, oneStep, setValues]
);
const stepUp = useCallback(
(i, step2 = oneStep) => {
const { values: values2 } = latestRef.current;
const value = values2[i];
constrain(i, reversed ? value - step2 : value + step2);
},
[constrain, reversed, latestRef, oneStep]
);
const stepDown = useCallback(
(i, step2 = oneStep) => {
const { values: values2 } = latestRef.current;
const value = values2[i];
constrain(i, reversed ? value + step2 : value - step2);
},
[constrain, reversed, latestRef, oneStep]
);
const reset = useCallback(
() => setValues(defaultValue),
[defaultValue, setValues]
);
const onKeyDown = useCallback(
(ev) => {
var _a;
const activeIndex = activeIndexRef.current;
const { valueBounds: valueBounds2 } = latestRef.current;
const { max: max2 = 100, min: min2 = 0 } = (_a = valueBounds2[activeIndex]) != null ? _a : {};
const actions = {
ArrowDown: () => stepDown(activeIndex),
ArrowLeft: () => stepDown(activeIndex),
ArrowRight: () => stepUp(activeIndex),
ArrowUp: () => stepUp(activeIndex),
End: () => constrain(activeIndex, max2),
Home: () => constrain(activeIndex, min2),
PageDown: () => stepDown(activeIndex, tenStep),
PageUp: () => stepUp(activeIndex, tenStep)
};
const action = actions[ev.key];
if (!action) return;
ev.preventDefault();
ev.stopPropagation();
action(ev);
eventSourceRef.current = "keyboard";
},
[constrain, latestRef, stepDown, stepUp, tenStep]
);
useUpdateEffect(() => {
const { values: values2 } = latestRef.current;
if (eventSourceRef.current === "keyboard") onChangeEnd(values2);
}, [startValue, endValue, onChangeEnd]);
const getContainerProps = useCallback(
(props2 = {}, ref = null) => {
var _a;
let w = "var(--ui-thumb-size)";
let h = "var(--ui-thumb-size)";
if (thumbSizes.length) {
const p = vertical ? "height" : "width";
const z = { height: 0, width: 0 };
const { height, width } = (_a = thumbSizes.reduce((a = z, b = z) => a[p] > b[p] ? a : b, z)) != null ? _a : {};
if (width) w = `${width}px`;
if (height) h = `${height}px`;
}
const paddingStyle = vertical ? { paddingLeft: `calc(${w} / 2)`, paddingRight: `calc(${w} / 2)` } : { paddingBottom: `calc(${h} / 2)`, paddingTop: `calc(${h} / 2)` };
const style = {
...props2.style,
outline: 0,
position: "relative",
touchAction: "none",
userSelect: "none",
WebkitTapHighlightColor: "rgba(0, 0, 0, 0)",
...paddingStyle
};
return {
...rest,
...props2,
id: `slider-container-${id}`,
ref: mergeRefs(ref, containerRef),
style,
tabIndex: -1,
vars: mergeVars(rest.vars, [
{
name: "thumb-size",
token: "sizes",
value: thumbSizeProp,
__prefix: "ui"
}
])
};
},
[id, vertical, rest, thumbSizeProp, thumbSizes]
);
const getInputProps = useCallback(
({ index: i, ...props2 }, ref = null) => ({
"aria-readonly": ariaReadonly,
...formControlProps,
...props2,
id: getInputId(i),
ref,
type: "hidden",
name: isArray(name) ? name[i] : `${name}-${i}`,
disabled,
readOnly,
required,
value: values[i]
}),
[
ariaReadonly,
disabled,
getInputId,
name,
readOnly,
required,
formControlProps,
values
]
);
const getTrackProps = useCallback(
(props2 = {}, ref = null) => {
const style = {
...props2.style,
position: "absolute",
...vertical ? {
height: "100%",
left: "50%",
transform: "translateX(-50%)"
} : {
top: "50%",
transform: "translateY(-50%)",
width: "100%"
}
};
return {
...formControlProps,
...props2,
id: `slider-track-${id}`,
ref: mergeRefs(ref, trackRef),
style
};
},
[id, vertical, formControlProps]
);
const getFilledTrackProps = useCallback(
(props2 = {}, ref = null) => {
const n = Math.abs(thumbPercents[1] - thumbPercents[0]);
const s = reversed ? 100 - thumbPercents[0] : thumbPercents[0];
const style = {
...props2.style,
position: "absolute",
...vertical ? {
height: `${n}%`,
left: "50%",
transform: "translateX(-50%)",
...reversed ? { top: `${s}%` } : { bottom: `${s}%` }
} : {
top: "50%",
transform: "translateY(-50%)",
width: `${n}%`,
...reversed ? { right: `${s}%` } : { left: `${s}%` }
}
};
return {
...formControlProps,
...props2,
id: `slider-filled-track-${id}`,
ref,
style
};
},
[id, reversed, vertical, formControlProps, thumbPercents]
);
const getMarkProps = useCallback(
(props2, ref = null) => {
let n = valueToPercent(props2.value, min, max);
n = reversed ? 100 - n : n;
const style = {
...props2.style,
pointerEvents: "none",
position: "absolute",
...vertical ? { bottom: `${n}%` } : { left: `${n}%` }
};
return {
...formControlProps,
...props2,
id: getMarkerId(props2.value),
ref,
style,
"aria-hidden": true,
"data-highlighted": dataAttr(
values[0] <= props2.value && props2.value <= values[1]
),
"data-invalid": dataAttr(props2.value < min || max < props2.value)
};
},
[getMarkerId, reversed, vertical, max, min, formControlProps, values]
);
const getThumbProps = useCallback(
({ index: i, ...props2 }, ref = null) => {
var _a, _b, _c;
const n = thumbPercents[i];
let w = "var(--ui-thumb-size)";
let h = "var(--ui-thumb-size)";
if (thumbSizes[i]) {
w = `${(_a = thumbSizes[i]) == null ? void 0 : _a.width}px`;
h = `${(_b = thumbSizes[i]) == null ? void 0 : _b.height}px`;
}
const bottom = `calc(${n}% - (${h} / 2))`;
const left = `calc(${n}% - (${w} / 2))`;
const style = {
...props2.style,
position: "absolute",
touchAction: "none",
userSelect: "none",
...vertical ? { bottom } : { left }
};
const value = values[i];
if (value == null)
throw new Error(
`Cannot find value at index '${i}'. The 'value' or 'defaultValue'`
);
return {
"aria-label": ariaLabel != null ? ariaLabel : "Slider thumb",
"aria-labelledby": ariaLabelledBy,
"aria-readonly": ariaReadonly,
...formControlProps,
...props2,
id: getThumbId(i),
ref,
style,
"aria-orientation": orientation,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": value,
"aria-valuetext": (_c = ariaValueText != null ? ariaValueText : getAriaValueText(value)) != null ? _c : value.toString(),
"data-active": dataAttr(
dragging && focusThumbOnChange && activeIndexRef.current === i
),
role: "slider",
tabIndex: interactive && focusThumbOnChange ? 0 : void 0,
onBlur: handlerAll(props2.onBlur, onBlur, () => {
activeIndexRef.current = -1;
setFocused(false);
}),
onFocus: handlerAll(props2.onFocus, onFocus, () => {
activeIndexRef.current = i;
setFocused(true);
}),
onKeyDown: handlerAll(props2.onKeyDown, onKeyDown)
};
},
[
thumbPercents,
thumbSizes,
vertical,
values,
ariaLabel,
ariaLabelledBy,
ariaReadonly,
formControlProps,
getThumbId,
orientation,
max,
min,
ariaValueText,
getAriaValueText,
dragging,
focusThumbOnChange,
interactive,
onBlur,
onFocus,
onKeyDown
]
);
return {
dragging,
focused,
getInputId,
getMarkerId,
getThumbId,
reset,
stepDown,
stepUp,
values,
vertical,
getContainerProps,
getFilledTrackProps,
getInputProps,
getMarkProps,
getThumbProps,
getTrackProps
};
};
var [RangeSliderProvider, useRangeSliderContext] = createContext({
name: "RangeSliderContext",
errorMessage: `useRangeSliderContext returned is 'undefined'. Seems you forgot to wrap the components in "<RangeSlider />" `
});
var RangeSlider = forwardRef((props, ref) => {
const [styles, mergedProps] = useComponentMultiStyle("RangeSlider", props);
const {
className,
children,
filledTrackColor,
thumbColor,
thumbSize,
trackColor,
trackSize,
filledTrackProps,
inputProps,
thumbProps,
trackProps,
...rest
} = omitThemeProps(mergedProps);
const {
vertical,
getContainerProps,
getFilledTrackProps,
getInputProps,
getMarkProps,
getThumbProps,
getTrackProps
} = useRangeSlider({ ...rest, thumbSize: getThumbSize(thumbSize, styles) });
const css = { ...styles.container };
const validChildren = getValidChildren(children);
const customRangeSliderTrack = findChild(validChildren, RangeSliderTrack);
const customRangeSliderStartThumb = findChild(
validChildren,
RangeSliderStartThumb
);
const customRangeSliderEndThumb = findChild(
validChildren,
RangeSliderEndThumb
);
const hasRangeSliderStartThumb = includesChildren(
validChildren,
RangeSliderStartThumb
);
const hasRangeSliderEndThumb = includesChildren(
validChildren,
RangeSliderEndThumb
);
const cloneChildren = !isEmpty(validChildren) ? omitChildren(
validChildren,
RangeSliderTrack,
RangeSliderStartThumb,
RangeSliderEndThumb
) : children;
return /* @__PURE__ */ jsx(
RangeSliderProvider,
{
value: {
filledTrackColor,
styles,
thumbColor,
thumbSize,
trackColor,
trackSize,
vertical,
filledTrackProps,
getFilledTrackProps,
getInputProps,
getMarkProps,
getThumbProps,
getTrackProps,
inputProps,
thumbProps,
trackProps
},
children: /* @__PURE__ */ jsxs(
ui.div,
{
className: cx("ui-slider", className),
__css: css,
...getContainerProps({}, ref),
children: [
customRangeSliderTrack != null ? customRangeSliderTrack : /* @__PURE__ */ jsx(RangeSliderTrack, {}),
cloneChildren,
customRangeSliderStartThumb != null ? customRangeSliderStartThumb : !hasRangeSliderStartThumb ? /* @__PURE__ */ jsx(RangeSliderStartThumb, {}) : null,
customRangeSliderEndThumb != null ? customRangeSliderEndThumb : !hasRangeSliderEndThumb ? /* @__PURE__ */ jsx(RangeSliderEndThumb, {}) : null
]
}
)
}
);
});
RangeSlider.displayName = "RangeSlider";
RangeSlider.__ui__ = "RangeSlider";
var RangeSliderTrack = forwardRef(
({ className, children, filledTrackProps, ...rest }, ref) => {
const {
styles,
trackColor,
trackSize,
vertical,
getTrackProps,
trackProps
} = useRangeSliderContext();
const css = { ...styles.track };
return /* @__PURE__ */ jsx(
ui.div,
{
className: cx("ui-slider__track", className),
__css: css,
...getTrackProps(
{
...trackColor ? { bg: trackColor } : {},
...trackSize ? vertical ? { w: trackSize } : { h: trackSize } : {},
...trackProps,
...rest
},
ref
),
children: children != null ? children : /* @__PURE__ */ jsx(RangeSliderFilledTrack, { ...filledTrackProps })
}
);
}
);
RangeSliderTrack.displayName = "RangeSliderTrack";
RangeSliderTrack.__ui__ = "RangeSliderTrack";
var RangeSliderFilledTrack = forwardRef(({ className, ...rest }, ref) => {
const { filledTrackColor, styles, filledTrackProps, getFilledTrackProps } = useRangeSliderContext();
const css = { ...styles.filledTrack };
return /* @__PURE__ */ jsx(
ui.div,
{
className: cx("ui-slider__track-filled", className),
__css: css,
...getFilledTrackProps(
{
...filledTrackColor ? { bg: filledTrackColor } : {},
...filledTrackProps,
...rest
},
ref
)
}
);
});
RangeSliderFilledTrack.displayName = "RangeSliderFilledTrack";
RangeSliderFilledTrack.__ui__ = "RangeSliderFilledTrack";
var RangeSliderMark = forwardRef(
({ className, ...rest }, ref) => {
const { styles, getMarkProps } = useRangeSliderContext();
const css = {
alignItems: "center",
display: "inline-flex",
justifyContent: "center",
...styles.mark
};
return /* @__PURE__ */ jsx(
ui.div,
{
className: cx("ui-slider__mark", className),
__css: css,
...getMarkProps(rest, ref)
}
);
}
);
RangeSliderMark.displayName = "RangeSliderMark";
RangeSliderMark.__ui__ = "RangeSliderMark";
var RangeSliderThumb = forwardRef(({ className, children, index, ...rest }, ref) => {
const {
styles,
thumbColor,
thumbSize,
getInputProps,
getThumbProps,
inputProps,
thumbProps
} = useRangeSliderContext();
const css = { ...styles.thumb };
const { children: propChildren } = thumbProps != null ? thumbProps : {};
return /* @__PURE__ */ jsxs(
ui.div,
{
className: cx("ui-slider__thumb", className),
__css: css,
...getThumbProps(
{
index,
...thumbColor ? { bg: thumbColor } : {},
...thumbSize ? { boxSize: thumbSize } : {},
...thumbProps,
...rest
},
ref
),
children: [
/* @__PURE__ */ jsx(ui.input, { ...getInputProps({ ...inputProps, index }, ref) }),
children != null ? children : propChildren
]
}
);
});
RangeSliderThumb.displayName = "RangeSliderThumb";
RangeSliderThumb.__ui__ = "RangeSliderThumb";
var RangeSliderStartThumb = forwardRef(
(rest, ref) => {
return /* @__PURE__ */ jsx(RangeSliderThumb, { ref, index: 0, ...rest });
}
);
RangeSliderStartThumb.displayName = "RangeSliderStartThumb";
RangeSliderStartThumb.__ui__ = "RangeSliderStartThumb";
var RangeSliderEndThumb = forwardRef(
(rest, ref) => {
return /* @__PURE__ */ jsx(RangeSliderThumb, { ref, index: 1, ...rest });
}
);
RangeSliderEndThumb.displayName = "RangeSliderEndThumb";
RangeSliderEndThumb.__ui__ = "RangeSliderEndThumb";
export {
useRangeSlider,
RangeSlider,
RangeSliderTrack,
RangeSliderFilledTrack,
RangeSliderMark,
RangeSliderStartThumb,
RangeSliderEndThumb
};
//# sourceMappingURL=chunk-E573AE7Z.mjs.map