UNPKG

@julo-ui/sliders

Version:

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

711 lines (690 loc) 22.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/slider/Slider.tsx var Slider_exports = {}; __export(Slider_exports, { default: () => Slider_default }); module.exports = __toCommonJS(Slider_exports); var import_system = require("@julo-ui/system"); // src/slider/use-slider.ts var import_react6 = require("react"); var import_function_utils3 = require("@julo-ui/function-utils"); var import_number_utils3 = require("@julo-ui/number-utils"); var import_dom_utils = require("@julo-ui/dom-utils"); var import_use_callback_ref = require("@julo-ui/use-callback-ref"); var import_use_controllable_state = require("@julo-ui/use-controllable-state"); var import_use_watch_element_size = require("@julo-ui/use-watch-element-size"); // src/usecase/use-handle-dragging.ts var import_react = require("react"); function useHandleDragging() { const [isDragging, setIsDragging] = (0, import_react.useState)(false); const onDraggingStart = (0, import_react.useCallback)(() => { setIsDragging(true); }, []); const onDraggingEnd = (0, import_react.useCallback)(() => { setIsDragging(false); }, []); return { isDragging, onDraggingStart, onDraggingEnd }; } var use_handle_dragging_default = useHandleDragging; // src/usecase/use-handle-focus.ts var import_react2 = require("react"); function useHandleFocus() { const [isFocused, setIsFocused] = (0, import_react2.useState)(false); const onFocus = (0, import_react2.useCallback)(() => { setIsFocused(true); }, []); const onBlur = (0, import_react2.useCallback)(() => { setIsFocused(false); }, []); return { isFocused, onFocus, onBlur }; } var use_handle_focus_default = useHandleFocus; // src/usecase/use-handle-reversed.ts function useHandleReversed(options) { const { isReversed = false, direction, orientation } = options; if (direction === "ltr" || orientation === "vertical") { return isReversed; } return !isReversed; } var use_handle_reversed_default = useHandleReversed; // src/usecase/use-handle-style.ts var import_react3 = require("react"); var defaultSize = { width: 0, height: 0 }; var normalizeSize = (size) => size || defaultSize; function useHandleStyle(options) { const { thumbSizes: thumbRects, orientation, isReversed, thumbPercents } = options; const getThumbStyle = (0, import_react3.useCallback)( (i) => { var _a; const rect = (_a = thumbRects[i]) != null ? _a : defaultSize; return { position: "absolute", userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", msUserSelect: "none", touchAction: "none", ...orientation === "vertical" ? { bottom: `calc(${thumbPercents[i]}% - ${rect.height / 2}px)` } : { left: `calc(${thumbPercents[i]}% - ${rect.width / 2}px)` } }; }, [orientation, thumbPercents, thumbRects] ); const getMarkerStyle = (0, import_react3.useCallback)( (percent) => { return { position: "absolute", pointerEvents: "none", ...orientation === "vertical" ? { bottom: `${percent}%`, transform: `translateY(-50%)` } : { left: `${percent}%`, transform: `translateX(-50%)` } }; }, [orientation] ); const rootStyle = (0, import_react3.useMemo)(() => { const size = normalizeSize( orientation === "vertical" ? thumbRects.reduce( (a, b) => normalizeSize(a).height > normalizeSize(b).height ? a : b, defaultSize ) : thumbRects.reduce( (a, b) => normalizeSize(a).width > normalizeSize(b).width ? a : b, defaultSize ) ); return { position: "relative", touchAction: "none", WebkitTapHighlightColor: "rgba(0,0,0,0)", userSelect: "none", outline: 0, ...orientation === "vertical" ? { paddingLeft: size.width / 2, paddingRight: size.width / 2, height: "100%" } : { paddingTop: size.height / 2, paddingBottom: size.height / 2, width: "100%" } }; }, [orientation, thumbRects]); const trackStyle = (0, import_react3.useMemo)( () => ({ position: "absolute", ...orientation === "vertical" ? { left: "50%", transform: "translateX(-50%)", height: "100%" } : { top: "50%", transform: "translateY(-50%)", width: "100%" } }), [orientation] ); const innerTrackStyle = (0, import_react3.useMemo)(() => { const isSingleThumb = thumbPercents.length === 1; const fallback = [ 0, isReversed ? 100 - thumbPercents[0] : thumbPercents[0] ]; const range = isSingleThumb ? fallback : thumbPercents; let start = range[0]; if (!isSingleThumb && isReversed) { start = 100 - start; } const percent = Math.abs(range[range.length - 1] - range[0]); return { ...trackStyle, ...orientation === "vertical" ? isReversed ? { height: `${percent}%`, top: `${start}%` } : { height: `${percent}%`, bottom: `${start}%` } : isReversed ? { width: `${percent}%`, right: `${start}%` } : { width: `${percent}%`, left: `${start}%` } }; }, [isReversed, orientation, thumbPercents, trackStyle]); return { getThumbStyle, rootStyle, trackStyle, innerTrackStyle, getMarkerStyle }; } var use_handle_style_default = useHandleStyle; // src/slider/usecase/use-handle-focus-thumb.ts var import_react4 = require("react"); var import_function_utils = require("@julo-ui/function-utils"); function useHandleFocusThumb(options) { const { focusThumbOnChange, thumbRef, eventSource, onChangeEnd = import_function_utils._noop, value } = options; const timeoutId = (0, import_react4.useRef)(); const onFocusThumb = (0, import_react4.useCallback)(() => { if (!focusThumbOnChange) return; timeoutId.current = setTimeout(() => { var _a; return (_a = thumbRef.current) == null ? void 0 : _a.focus(); }); }, [focusThumbOnChange, thumbRef]); (0, import_react4.useEffect)(() => { onFocusThumb(); if (eventSource === "keyboard") { onChangeEnd(value); } }, [eventSource, onChangeEnd, onFocusThumb, value]); (0, import_react4.useEffect)(() => { return () => clearTimeout(timeoutId.current); }, []); return { onFocusThumb }; } var use_handle_focus_thumb_default = useHandleFocusThumb; // src/slider/usecase/use-handle-pan-event.ts var import_react5 = require("react"); var import_react_use_pan_event = require("@chakra-ui/react-use-pan-event"); var import_function_utils2 = require("@julo-ui/function-utils"); var import_number_utils2 = require("@julo-ui/number-utils"); // src/utils.ts var import_number_utils = require("@julo-ui/number-utils"); function valueToPercent(value, min, max) { return (value - min) * 100 / (max - min); } function percentToValue(percent, min, max) { return (max - min) * percent + min; } function isMouseEvent(event) { return !("touches" in event); } function roundValueToStep(value, from, step) { const nextValue = Math.round((value - from) / step) * step + from; const precision = (0, import_number_utils.countDecimalPlaces)(step); return (0, import_number_utils.toPreciseDecimal)(nextValue, precision); } // src/slider/usecase/use-handle-pan-event.ts function useHandlePanEvent(options) { const { rootRef, onDraggingEnd, onDraggingStart, onFocusThumb, onChangeStart = import_function_utils2._noop, onChangeEnd = import_function_utils2._noop, sliderStates, trackRef, setComputedValue } = options; const getValueFromPointer = (0, import_react5.useCallback)( (event) => { var _a; if (!trackRef.current) return; sliderStates.eventSource = "pointer"; const trackRect = trackRef.current.getBoundingClientRect(); const { clientX, clientY } = !isMouseEvent(event) ? (_a = event.touches) == null ? void 0 : _a[0] : event; const diff = sliderStates.isVertical ? trackRect.bottom - clientY : clientX - trackRect.left; const length = sliderStates.isVertical ? trackRect.height : trackRect.width; let percent = diff / length; if (sliderStates.isReversed) { percent = 1 - percent; } let nextValue = percentToValue( percent, sliderStates.min, sliderStates.max ); if (sliderStates.step) { nextValue = parseFloat( roundValueToStep(nextValue, sliderStates.min, sliderStates.step) ); } nextValue = (0, import_number_utils2.clampValue)(nextValue, sliderStates.min, sliderStates.max); return nextValue; }, [trackRef, sliderStates] ); function setValueFromPointer(event) { const nextValue = getValueFromPointer(event); if (nextValue != null && nextValue !== sliderStates.value) { setComputedValue(nextValue); } } (0, import_react_use_pan_event.usePanEvent)(rootRef, { onPanSessionStart(event) { if (!sliderStates.isInteractive) return; onDraggingStart(); onFocusThumb(); setValueFromPointer(event); onChangeStart(sliderStates.value); }, onPanSessionEnd() { if (!sliderStates.isInteractive) return; onDraggingEnd(); onChangeEnd(sliderStates.value); }, onPan(event) { if (!sliderStates.isInteractive) return; setValueFromPointer(event); } }); } var use_handle_pan_event_default = useHandlePanEvent; // src/slider/utils.ts function getDefaultValue(min, max) { return max < min ? min : min + (max - min) / 2; } // src/slider/use-slider.ts function useSlider(props) { var _a; 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, ...resRootProps } = props; const onChangeStart = (0, import_use_callback_ref.useCallbackRef)(onChangeStartProp); const onChangeEnd = (0, import_use_callback_ref.useCallbackRef)(onChangeEndProp); const getAriaValueText = (0, import_use_callback_ref.useCallbackRef)(getAriaValueTextProp); const isReversed = use_handle_reversed_default({ isReversed: isReversedProp, direction, orientation }); const [computedValue, setComputedValue] = (0, import_use_controllable_state.useControllableState)({ value: valueProp, defaultValue: defaultValue != null ? defaultValue : getDefaultValue(min, max), onChange }); const isInteractive = !(isDisabled || isReadOnly); const tenSteps = (max - min) / 10; const oneStep = step || (max - min) / 100; const value = (0, import_number_utils3.clampValue)(computedValue, min, max); const reversedValue = max - value + min; const trackValue = isReversed ? reversedValue : value; const thumbPercent = valueToPercent(trackValue, min, max); const isVertical = orientation === "vertical"; const trackRef = (0, import_react6.useRef)(null); const thumbRef = (0, import_react6.useRef)(null); const rootRef = (0, import_react6.useRef)(null); const reactId = (0, import_react6.useId)(); const uuid = idProp != null ? idProp : reactId; const thumbId = `slider-thumb-${uuid}`; const trackId = `slider-track-${uuid}`; const valueText = (_a = getAriaValueText == null ? void 0 : getAriaValueText(value)) != null ? _a : ariaValueText; const thumbSize = (0, import_use_watch_element_size.useWatchElementSize)(thumbRef); const { isDragging, onDraggingStart, onDraggingEnd } = use_handle_dragging_default(); const { isFocused, onBlur: onInputBlur, onFocus: onInputFocus } = use_handle_focus_default(); const sliderStates = (0, import_react6.useMemo)( () => ({ tenSteps, min, max, step: oneStep, isDisabled, value, isInteractive, isReversed, isVertical, eventSource: null, focusThumbOnChange, orientation, isDragging, isFocused }), [ focusThumbOnChange, isDisabled, isDragging, isFocused, isInteractive, isReversed, isVertical, max, min, oneStep, orientation, tenSteps, value ] ); const constrain = (0, import_react6.useCallback)( (value2) => { if (!sliderStates.isInteractive) return; value2 = parseFloat(roundValueToStep(value2, sliderStates.min, oneStep)); value2 = (0, import_number_utils3.clampValue)(value2, sliderStates.min, sliderStates.max); setComputedValue(value2); }, [ oneStep, setComputedValue, sliderStates.isInteractive, sliderStates.max, sliderStates.min ] ); const actions = (0, import_react6.useMemo)( () => ({ stepUp(step2 = oneStep) { const next = isReversed ? value - step2 : value + step2; constrain(next); }, stepDown(step2 = oneStep) { const next = isReversed ? value + step2 : value - step2; constrain(next); }, reset() { constrain(defaultValue || 0); }, stepTo(value2) { constrain(value2); } }), [constrain, isReversed, value, oneStep, defaultValue] ); const { getThumbStyle, getMarkerStyle, innerTrackStyle, rootStyle, trackStyle } = use_handle_style_default({ isReversed: sliderStates.isReversed, orientation: sliderStates.orientation, thumbPercents: [thumbPercent], thumbSizes: [thumbSize] }); const { onFocusThumb } = use_handle_focus_thumb_default({ eventSource: sliderStates.eventSource, focusThumbOnChange: sliderStates.focusThumbOnChange, onChangeEnd, thumbRef, value: sliderStates.value }); use_handle_pan_event_default({ onChangeEnd, onChangeStart, onDraggingEnd, onDraggingStart, onFocusThumb, rootRef, setComputedValue, sliderStates, trackRef }); const onThumbKeyDown = (0, import_react6.useCallback)( (event) => { const keyMap = { ArrowRight: () => actions.stepUp(), ArrowUp: () => actions.stepUp(), ArrowLeft: () => actions.stepDown(), ArrowDown: () => actions.stepDown(), PageUp: () => actions.stepUp(tenSteps), PageDown: () => actions.stepDown(tenSteps), Home: () => constrain(sliderStates.min), End: () => constrain(sliderStates.max) }; const action = keyMap[event.key]; if (action) { event.preventDefault(); event.stopPropagation(); action(event); sliderStates.eventSource = "keyboard"; } }, [actions, constrain, tenSteps, sliderStates] ); const getRootProps = (0, import_react6.useCallback)( (props2 = {}, forwardedRef = null) => ({ ...props2, ...resRootProps, ref: (0, import_dom_utils.mergeRefs)(forwardedRef, rootRef), tabIndex: -1, "aria-disabled": (0, import_dom_utils.ariaAttr)(isDisabled), "data-focused": (0, import_dom_utils.dataAttr)(isFocused), style: { ...props2.style, ...rootStyle } }), [resRootProps, isDisabled, isFocused, rootStyle] ); const getTrackProps = (0, import_react6.useCallback)( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: (0, import_dom_utils.mergeRefs)(forwardedRef, trackRef), id: trackId, "data-disabled": (0, import_dom_utils.dataAttr)(isDisabled), style: { ...props2.style, ...trackStyle } }), [isDisabled, trackId, trackStyle] ); const getInnerTrackProps = (0, import_react6.useCallback)( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: forwardedRef, style: { ...props2.style, ...innerTrackStyle } }), [innerTrackStyle] ); const getThumbProps = (0, import_react6.useCallback)( (props2 = {}, forwardedRef = null) => { const { onKeyDown, onFocus, onBlur, style, ...resProps } = props2; return { ...resProps, ref: (0, import_dom_utils.mergeRefs)(forwardedRef, thumbRef), role: "slider", ...isInteractive && { tabIndex: 0 }, id: thumbId, "data-active": (0, import_dom_utils.dataAttr)(isDragging), "aria-valuetext": valueText, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, "aria-orientation": orientation, "aria-disabled": (0, import_dom_utils.ariaAttr)(isDisabled), "aria-readonly": (0, import_dom_utils.ariaAttr)(isReadOnly), "aria-label": ariaLabel, ...!ariaLabel && { "aria-labelledby": ariaLabelledBy }, style: { ...style, ...getThumbStyle(0) }, onKeyDown: (0, import_function_utils3.callAllFn)(onThumbKeyDown, onKeyDown), onFocus: (0, import_function_utils3.callAllFn)(onInputFocus, onFocus), onblur: (0, import_function_utils3.callAllFn)(onInputBlur, onBlur) }; }, [ ariaLabel, ariaLabelledBy, getThumbStyle, isDisabled, isDragging, isInteractive, isReadOnly, max, min, onInputBlur, onInputFocus, onThumbKeyDown, orientation, thumbId, value, valueText ] ); const getMarkerProps = (0, import_react6.useCallback)( (props2, forwardedRef = null) => { const { value: markerValue, style, ...resProps } = props2; const isInRange = !(markerValue < min || markerValue > max); const isHighlighted = value >= markerValue; const markerPercent = valueToPercent(markerValue, min, max); const percent = isReversed ? 100 - markerPercent : markerPercent; return { ...resProps, ref: forwardedRef, role: "presentation", "aria-hidden": true, "data-disabled": (0, import_dom_utils.dataAttr)(isDisabled), "data-invalid": (0, import_dom_utils.dataAttr)(!isInRange), "data-highlighted": (0, import_dom_utils.dataAttr)(isHighlighted), style: { ...style, ...getMarkerStyle(percent) } }; }, [getMarkerStyle, isDisabled, isReversed, max, min, value] ); const getInputProps = (0, import_react6.useCallback)( (props2 = {}, forwardedRef = null) => ({ ...props2, ref: forwardedRef, type: "hidden", value, name }), [name, value] ); return { state: sliderStates, actions, getRootProps, getTrackProps, getInnerTrackProps, getThumbProps, getMarkerProps, getInputProps }; } // src/slider/styles.ts var import_react7 = require("@emotion/react"); var sliderCx = import_react7.css` cursor: pointer; `; // src/slider/SliderProvider.tsx var import_context = require("@julo-ui/context"); var [SliderProvider, useSliderContext] = (0, import_context.createContext)({ name: "SliderContext", hookName: "useSliderContext", providerName: "<SliderProvider />" }); // src/styles.ts var import_react8 = require("@emotion/react"); var rootSliderCx = import_react8.css` --slider-track-size: 0.5rem; --slider-thumb-size: 1rem; width: fit-content; `; var rootSliderVerticalTrackCx = import_react8.css` width: var(--slider-track-size); `; var rootSliderHorizontalTrackCx = import_react8.css` height: var(--slider-track-size); `; var rootSliderTrackCx = import_react8.css` overflow: hidden; border-radius: var(--corner-3xl); background-color: var(--colors-neutrals-40); `; var rootSliderThumbCx = import_react8.css` z-index: 1; width: var(--slider-thumb-size); height: var(--slider-thumb-size); border-radius: var(--corner-3xl); background-color: var(--colors-neutrals-10); box-shadow: var(--shadows-md); display: flex; align-items: center; justify-content: center; `; var rootSliderInnerTrackCx = import_react8.css` background-color: var(--colors-primary-30); height: inherit; width: inherit; `; // src/slider/Slider.tsx var import_jsx_runtime = require("react/jsx-runtime"); var Slider = (0, import_system.forwardRef)((props, ref) => { const { children, className, orientation = "horizontal", inputRef, inputProps, ...resProps } = props; const { getInputProps, getRootProps, ...sliderContext } = useSlider({ direction: "ltr", orientation, ...resProps }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SliderProvider, { value: sliderContext, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_system.julo.div, { className: (0, import_system.cx)("julo-slider", className), ...getRootProps({}, ref), __css: [rootSliderCx, sliderCx], children: [ children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_system.julo.input, { className: "julo-slider__input", ...getInputProps(inputProps, inputRef) } ) ] } ) }); }); Slider.displayName = "Slider"; var Slider_default = Slider; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = {});