UNPKG

@yamada-ui/slider

Version:

Yamada UI slider components

736 lines (734 loc) • 21.8 kB
"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