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