@yamada-ui/slider
Version:
Yamada UI slider components
623 lines (621 loc) • 18.4 kB
JavaScript
"use client"
// src/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 { useSize } from "@yamada-ui/use-size";
import {
clampNumber,
createContext,
cx,
dataAttr,
findChild,
getValidChildren,
handlerAll,
includesChildren,
isEmpty,
isNumber,
mergeRefs,
omitChildren,
percentToValue,
pickObject,
roundNumberToStep,
useCallbackRef,
useUpdateEffect,
valueToPercent
} from "@yamada-ui/utils";
import { useCallback, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
var getThumbSize = (thumbSize, styles) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
return (_t = (_s = (_q = (_o = (_m = (_k = (_i = (_g = (_e = (_c = thumbSize != null ? thumbSize : (_a = styles.thumb) == null ? void 0 : _a.boxSize) != null ? _c : (_b = styles.thumb) == null ? void 0 : _b.minBoxSize) != null ? _e : (_d = styles.thumb) == null ? void 0 : _d.width) != null ? _g : (_f = styles.thumb) == null ? void 0 : _f.w) != null ? _i : (_h = styles.thumb) == null ? void 0 : _h.minWidth) != null ? _k : (_j = styles.thumb) == null ? void 0 : _j.minW) != null ? _m : (_l = styles.thumb) == null ? void 0 : _l.height) != null ? _o : (_n = styles.thumb) == null ? void 0 : _n.h) != null ? _q : (_p = styles.thumb) == null ? void 0 : _p.minHeight) != null ? _s : (_r = styles.thumb) == null ? void 0 : _r.minH) != null ? _t : "3.5";
};
var useSlider = ({
focusThumbOnChange = true,
...props
}) => {
if (!focusThumbOnChange) props.isReadOnly = true;
const {
id,
name,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledBy,
"aria-valuetext": ariaValueText,
defaultValue,
getAriaValueText: getAriaValueTextProp,
isReversed,
max = 100,
min = 0,
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 [computedValue, setValue] = useControllableState({
defaultValue: defaultValue != null ? defaultValue : min + (max - min) / 2,
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 value = clampNumber(computedValue, min, max);
const reversedValue = max - value + min;
const thumbValue = reversed ? reversedValue : value;
const thumbPercent = valueToPercent(thumbValue, min, max);
const vertical = orientation === "vertical";
const latestRef = useLatestRef({
focusThumbOnChange,
interactive,
max,
min,
step,
value
});
const eventSourceRef = useRef(null);
const containerRef = useRef(null);
const trackRef = useRef(null);
const thumbRef = useRef(null);
const thumbSize = useSize(thumbRef);
const onChangeStart = useCallbackRef(onChangeStartProp);
const onChangeEnd = useCallbackRef(onChangeEndProp);
const getAriaValueText = useCallbackRef(getAriaValueTextProp);
usePanEvent(containerRef, {
onMove: (ev) => {
const { interactive: interactive2 } = latestRef.current;
if (!interactive2) return;
setValueFromPointer(ev);
},
onSessionEnd: () => {
const { interactive: interactive2, value: value2 } = latestRef.current;
if (!interactive2) return;
setDragging(false);
onChangeEnd(value2);
},
onSessionStart: (ev) => {
const { interactive: interactive2, value: value2 } = latestRef.current;
if (!interactive2) return;
setDragging(true);
focusThumb();
setValueFromPointer(ev);
onChangeStart(value2);
}
});
const getValueFromPointer = useCallback(
(ev) => {
var _a, _b;
if (!trackRef.current) return;
const { max: max2, min: min2, step: step2 } = 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);
if (step2) nextValue = parseFloat(roundNumberToStep(nextValue, min2, step2));
nextValue = clampNumber(nextValue, min2, max2);
return nextValue;
},
[vertical, reversed, latestRef]
);
const setValueFromPointer = (ev) => {
const { value: value2 } = latestRef.current;
const nextValue = getValueFromPointer(ev);
if (nextValue != null && nextValue !== value2) setValue(nextValue);
};
const focusThumb = useCallback(() => {
const { focusThumbOnChange: focusThumbOnChange2 } = latestRef.current;
if (focusThumbOnChange2) setTimeout(() => {
var _a;
return (_a = thumbRef.current) == null ? void 0 : _a.focus();
});
}, [latestRef]);
const constrain = useCallback(
(value2) => {
const { interactive: interactive2, max: max2, min: min2 } = latestRef.current;
if (!interactive2) return;
value2 = parseFloat(roundNumberToStep(value2, min2, oneStep));
value2 = clampNumber(value2, min2, max2);
setValue(value2);
},
[oneStep, setValue, latestRef]
);
const stepUp = useCallback(
(step2 = oneStep) => constrain(reversed ? value - step2 : value + step2),
[constrain, reversed, oneStep, value]
);
const stepDown = useCallback(
(step2 = oneStep) => constrain(reversed ? value + step2 : value - step2),
[constrain, reversed, oneStep, value]
);
const reset = useCallback(
() => constrain(defaultValue || 0),
[constrain, defaultValue]
);
const stepTo = useCallback((value2) => constrain(value2), [constrain]);
const onKeyDown = useCallback(
(ev) => {
const { max: max2, min: min2 } = latestRef.current;
const actions = {
ArrowDown: () => stepDown(),
ArrowLeft: () => stepDown(),
ArrowRight: () => stepUp(),
ArrowUp: () => stepUp(),
End: () => constrain(max2),
Home: () => constrain(min2),
PageDown: () => stepDown(tenStep),
PageUp: () => stepUp(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 { value: value2 } = latestRef.current;
focusThumb();
if (eventSourceRef.current === "keyboard") onChangeEnd(value2);
}, [value, onChangeEnd]);
const getContainerProps = useCallback(
(props2 = {}, ref = null) => {
let { height: h, width: w } = thumbSize != null ? thumbSize : {
height: "var(--ui-thumb-size)",
width: "var(--ui-thumb-size)"
};
if (isNumber(w)) w = `${w}px`;
if (isNumber(h)) h = `${h}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,
ref: mergeRefs(ref, containerRef),
style,
tabIndex: -1,
vars: mergeVars(rest.vars, [
{
name: "thumb-size",
token: "sizes",
value: thumbSizeProp,
__prefix: "ui"
}
])
};
},
[vertical, rest, thumbSize, thumbSizeProp]
);
const getInputProps = useCallback(
(props2 = {}, ref = null) => ({
"aria-readonly": ariaReadonly,
...formControlProps,
...props2,
id,
ref,
type: "hidden",
name,
disabled,
readOnly,
required,
value
}),
[
ariaReadonly,
disabled,
formControlProps,
id,
name,
readOnly,
required,
value
]
);
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,
ref: mergeRefs(ref, trackRef),
style
};
},
[vertical, formControlProps]
);
const getFilledTrackProps = useCallback(
(props2 = {}, ref = null) => {
const n = Math.abs(reversed ? 100 - thumbPercent : thumbPercent);
const style = {
...props2.style,
position: "absolute",
...vertical ? {
height: `${n}%`,
left: "50%",
transform: "translateX(-50%)",
...reversed ? { top: "0%" } : { bottom: "0%" }
} : {
top: "50%",
transform: "translateY(-50%)",
width: `${n}%`,
...reversed ? { right: "0%" } : { left: "0%" }
}
};
return {
...formControlProps,
...props2,
ref,
style
};
},
[reversed, vertical, formControlProps, thumbPercent]
);
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,
ref,
style,
"aria-hidden": true,
"data-highlighted": dataAttr(props2.value <= value),
"data-invalid": dataAttr(props2.value < min || max < props2.value)
};
},
[reversed, vertical, max, min, formControlProps, value]
);
const getThumbProps = useCallback(
(props2 = {}, ref = null) => {
var _a;
const n = thumbPercent;
let w = "var(--ui-thumb-size)";
let h = "var(--ui-thumb-size)";
if (thumbSize) {
w = `${thumbSize.width}px`;
h = `${thumbSize.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 }
};
return {
"aria-label": ariaLabel != null ? ariaLabel : "Slider thumb",
"aria-labelledby": ariaLabelledBy,
"aria-readonly": ariaReadonly,
...formControlProps,
...props2,
ref: mergeRefs(ref, thumbRef),
style,
"aria-orientation": orientation,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": value,
"aria-valuetext": (_a = ariaValueText != null ? ariaValueText : getAriaValueText(value)) != null ? _a : value.toString(),
"data-active": dataAttr(dragging && focusThumbOnChange),
role: "slider",
tabIndex: interactive && focusThumbOnChange ? 0 : void 0,
onBlur: handlerAll(props2.onBlur, onBlur, () => setFocused(false)),
onFocus: handlerAll(props2.onFocus, onFocus, () => setFocused(true)),
onKeyDown: handlerAll(props2.onKeyDown, onKeyDown)
};
},
[
thumbPercent,
thumbSize,
vertical,
ariaLabel,
ariaLabelledBy,
ariaReadonly,
formControlProps,
orientation,
max,
min,
value,
ariaValueText,
getAriaValueText,
dragging,
focusThumbOnChange,
interactive,
onBlur,
onFocus,
onKeyDown
]
);
return {
dragging,
focused,
/**
* @deprecated Use `dragging` instead.
*/
isDragging: dragging,
/**
* @deprecated Use `focused` instead.
*/
isFocused: focused,
/**
* @deprecated Use `vertical` instead.
*/
isVertical: vertical,
reset,
stepDown,
stepTo,
stepUp,
value,
vertical,
getContainerProps,
getFilledTrackProps,
getInputProps,
getMarkProps,
getThumbProps,
getTrackProps
};
};
var [SliderProvider, useSliderContext] = createContext({
name: "SliderContext",
errorMessage: `useSliderContext returned is 'undefined'. Seems you forgot to wrap the components in "<Slider />" `
});
var Slider = forwardRef((props, ref) => {
const [styles, mergedProps] = useComponentMultiStyle("Slider", props);
const {
className,
children,
filledTrackColor,
thumbColor,
thumbSize,
trackColor,
trackSize,
filledTrackProps,
inputProps,
thumbProps,
trackProps,
...rest
} = omitThemeProps(mergedProps);
const {
isVertical,
getContainerProps,
getFilledTrackProps,
getInputProps,
getMarkProps,
getThumbProps,
getTrackProps
} = useSlider({ ...rest, thumbSize: getThumbSize(thumbSize, styles) });
const css = { ...styles.container };
const validChildren = getValidChildren(children);
const customSliderTrack = findChild(validChildren, SliderTrack);
const customSliderThumb = findChild(validChildren, SliderThumb);
const hasSliderThumb = includesChildren(validChildren, SliderThumb);
const cloneChildren = !isEmpty(validChildren) ? omitChildren(validChildren, SliderTrack, SliderThumb) : children;
return /* @__PURE__ */ jsx(
SliderProvider,
{
value: {
filledTrackColor,
isVertical,
styles,
thumbColor,
thumbSize,
trackColor,
trackSize,
filledTrackProps,
getFilledTrackProps,
getMarkProps,
getThumbProps,
getTrackProps,
thumbProps,
trackProps
},
children: /* @__PURE__ */ jsxs(
ui.div,
{
className: cx("ui-slider", className),
__css: css,
...getContainerProps(),
children: [
/* @__PURE__ */ jsx(ui.input, { ...getInputProps(inputProps, ref) }),
customSliderTrack != null ? customSliderTrack : /* @__PURE__ */ jsx(SliderTrack, {}),
cloneChildren,
customSliderThumb != null ? customSliderThumb : !hasSliderThumb ? /* @__PURE__ */ jsx(SliderThumb, {}) : null
]
}
)
}
);
});
Slider.displayName = "Slider";
Slider.__ui__ = "Slider";
var SliderTrack = forwardRef(
({ className, children, filledTrackProps, ...rest }, ref) => {
const {
isVertical,
styles,
trackColor,
trackSize,
getTrackProps,
trackProps
} = useSliderContext();
const css = { ...styles.track };
return /* @__PURE__ */ jsx(
ui.div,
{
className: cx("ui-slider__track", className),
__css: css,
...getTrackProps(
{
...trackColor ? { bg: trackColor } : {},
...trackSize ? isVertical ? { w: trackSize } : { h: trackSize } : {},
...trackProps,
...rest
},
ref
),
children: children != null ? children : /* @__PURE__ */ jsx(SliderFilledTrack, { ...filledTrackProps })
}
);
}
);
SliderTrack.displayName = "SliderTrack";
SliderTrack.__ui__ = "SliderTrack";
var SliderFilledTrack = forwardRef(
({ className, ...rest }, ref) => {
const { filledTrackColor, styles, filledTrackProps, getFilledTrackProps } = useSliderContext();
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
)
}
);
}
);
SliderFilledTrack.displayName = "SliderFilledTrack";
SliderFilledTrack.__ui__ = "SliderFilledTrack";
var SliderMark = forwardRef(
({ className, ...rest }, ref) => {
const { styles, getMarkProps } = useSliderContext();
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)
}
);
}
);
SliderMark.displayName = "SliderMark";
SliderMark.__ui__ = "SliderMark";
var SliderThumb = forwardRef(
({ className, ...rest }, ref) => {
const { styles, thumbColor, thumbSize, getThumbProps, thumbProps } = useSliderContext();
const css = { ...styles.thumb };
return /* @__PURE__ */ jsx(
ui.div,
{
className: cx("ui-slider__thumb", className),
__css: css,
...getThumbProps(
{
...thumbColor ? { bg: thumbColor } : {},
...thumbSize ? { boxSize: thumbSize } : {},
...thumbProps,
...rest
},
ref
)
}
);
}
);
SliderThumb.displayName = "SliderThumb";
SliderThumb.__ui__ = "SliderThumb";
export {
getThumbSize,
useSlider,
Slider,
SliderTrack,
SliderFilledTrack,
SliderMark,
SliderThumb
};
//# sourceMappingURL=chunk-FXWAONIG.mjs.map