@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
780 lines (773 loc) • 25.8 kB
JavaScript
import { getNextSortedValues, hasMinStepsBetweenValues, stopEventDefaultAndPropagation, getClosestValueIndex, linearScale } from './YSW4UOAR.js';
import { createDomCollectionItem, createDomCollection } from './7CVNMTYF.js';
import { createNumberFormatter, useLocale } from './XHJPQEZP.js';
import { FORM_CONTROL_FIELD_PROP_NAMES, createFormControlField } from './HYP2U57X.js';
import { FormControlLabel } from './7ZHN3PYD.js';
import { createFormResetListener } from './ANN3A2QM.js';
import { FormControlErrorMessage } from './ICNSTULC.js';
import { FormControlDescription, useFormControlContext, FORM_CONTROL_PROP_NAMES, createFormControl, FormControlContext } from './YKGT7A57.js';
import { createControllableArraySignal } from './BLN63FDC.js';
import { Polymorphic } from './6Y7B2NEO.js';
import { __export } from './5ZKAE4VZ.js';
import { createComponent, mergeProps, spread, template } from 'solid-js/web';
import { createContext, useContext, splitProps, createUniqueId, onMount, createSignal, createEffect, createMemo } from 'solid-js';
import { combineStyle } from '@solid-primitives/props';
import { mergeDefaultProps, mergeRefs, visuallyHiddenStyles, createGenerateId, access, callHandler, snapValueToStep, clamp } from '@kobalte/utils';
// src/slider/index.tsx
var slider_exports = {};
__export(slider_exports, {
Description: () => FormControlDescription,
ErrorMessage: () => FormControlErrorMessage,
Fill: () => SliderFill,
Input: () => SliderInput,
Label: () => FormControlLabel,
Root: () => SliderRoot,
Slider: () => Slider,
Thumb: () => SliderThumb,
Track: () => SliderTrack,
ValueLabel: () => SliderValueLabel,
useSliderContext: () => useSliderContext
});
var SliderContext = createContext();
function useSliderContext() {
const context = useContext(SliderContext);
if (context === void 0) {
throw new Error("[kobalte]: `useSliderContext` must be used within a `Slider.Root` component");
}
return context;
}
// src/slider/slider-fill.tsx
function SliderFill(props) {
const context = useSliderContext();
const [local, others] = splitProps(props, ["style"]);
const percentages = () => {
return context.state.values().map((value) => context.state.getValuePercent(value) * 100);
};
const offsetStart = () => {
return context.state.values().length > 1 ? Math.min(...percentages()) : 0;
};
const offsetEnd = () => {
return 100 - Math.max(...percentages());
};
return createComponent(Polymorphic, mergeProps({
as: "div",
get style() {
return combineStyle({
[context.startEdge()]: `${offsetStart()}%`,
[context.endEdge()]: `${offsetEnd()}%`
}, local.style);
}
}, () => context.dataset(), others));
}
function SliderThumb(props) {
let ref;
const context = useSliderContext();
const mergedProps = mergeDefaultProps({
id: context.generateId(`thumb-${createUniqueId()}`)
}, props);
const [local, formControlFieldProps, others] = splitProps(mergedProps, ["ref", "style", "onKeyDown", "onPointerDown", "onPointerMove", "onPointerUp", "onFocus", "onBlur"], FORM_CONTROL_FIELD_PROP_NAMES);
const {
fieldProps
} = createFormControlField(formControlFieldProps);
createDomCollectionItem({
getItem: () => ({
ref: () => ref,
disabled: context.state.isDisabled(),
key: fieldProps.id(),
textValue: "",
type: "item"
})
});
const index = () => ref ? context.thumbs().findIndex((v) => v.ref() === ref) : -1;
const value = () => context.state.getThumbValue(index());
const position = () => {
return context.state.getThumbPercent(index());
};
const transform = () => {
if (context.state.orientation() === "vertical") {
return context.inverted() ? "translateY(-50%)" : "translateY(50%)";
}
return context.inverted() ? "translateX(50%)" : "translateX(-50%)";
};
let startPosition = 0;
const onKeyDown = (e) => {
callHandler(e, local.onKeyDown);
context.onStepKeyDown(e, index());
};
const onPointerDown = (e) => {
callHandler(e, local.onPointerDown);
const target = e.currentTarget;
e.preventDefault();
e.stopPropagation();
target.setPointerCapture(e.pointerId);
target.focus();
startPosition = context.state.orientation() === "horizontal" ? e.clientX : e.clientY;
if (value() !== void 0) {
context.onSlideStart?.(index(), value());
}
};
const onPointerMove = (e) => {
e.stopPropagation();
callHandler(e, local.onPointerMove);
const target = e.currentTarget;
if (target.hasPointerCapture(e.pointerId)) {
const delta = {
deltaX: e.clientX - startPosition,
deltaY: e.clientY - startPosition
};
context.onSlideMove?.(delta);
startPosition = context.state.orientation() === "horizontal" ? e.clientX : e.clientY;
}
};
const onPointerUp = (e) => {
e.stopPropagation();
callHandler(e, local.onPointerUp);
const target = e.currentTarget;
if (target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId);
context.onSlideEnd?.();
}
};
const onFocus = (e) => {
callHandler(e, local.onFocus);
context.state.setFocusedThumb(index());
};
const onBlur = (e) => {
callHandler(e, local.onBlur);
context.state.setFocusedThumb(void 0);
};
onMount(() => {
context.state.setThumbEditable(index(), !context.state.isDisabled());
});
return createComponent(ThumbContext.Provider, {
value: {
index
},
get children() {
return createComponent(Polymorphic, mergeProps({
as: "span",
ref(r$) {
const _ref$ = mergeRefs((el) => ref = el, local.ref);
typeof _ref$ === "function" && _ref$(r$);
},
role: "slider",
get id() {
return fieldProps.id();
},
get tabIndex() {
return context.state.isDisabled() ? void 0 : 0;
},
get style() {
return combineStyle({
display: value() === void 0 ? "none" : void 0,
position: "absolute",
[context.startEdge()]: `calc(${position() * 100}%)`,
transform: transform(),
"touch-action": "none"
}, local.style);
},
get ["aria-valuetext"]() {
return context.state.getThumbValueLabel(index());
},
get ["aria-valuemin"]() {
return context.minValue();
},
get ["aria-valuenow"]() {
return value();
},
get ["aria-valuemax"]() {
return context.maxValue();
},
get ["aria-orientation"]() {
return context.state.orientation();
},
get ["aria-label"]() {
return fieldProps.ariaLabel();
},
get ["aria-labelledby"]() {
return fieldProps.ariaLabelledBy();
},
get ["aria-describedby"]() {
return fieldProps.ariaDescribedBy();
},
onKeyDown,
onPointerDown,
onPointerMove,
onPointerUp,
onFocus,
onBlur
}, () => context.dataset(), others));
}
});
}
var ThumbContext = createContext();
function useThumbContext() {
const context = useContext(ThumbContext);
if (context === void 0) {
throw new Error("[kobalte]: `useThumbContext` must be used within a `Slider.Thumb` component");
}
return context;
}
// src/slider/slider-input.tsx
var _tmpl$ = /* @__PURE__ */ template(`<input type="range">`);
function SliderInput(props) {
const formControlContext = useFormControlContext();
const context = useSliderContext();
const thumb = useThumbContext();
const mergedProps = mergeDefaultProps({
id: context.generateId("input")
}, props);
const [local, formControlFieldProps, others] = splitProps(mergedProps, ["style", "onChange"], FORM_CONTROL_FIELD_PROP_NAMES);
const {
fieldProps
} = createFormControlField(formControlFieldProps);
const [valueText, setValueText] = createSignal("");
const onChange = (e) => {
callHandler(e, local.onChange);
const target = e.target;
context.state.setThumbValue(thumb.index(), Number.parseFloat(target.value));
target.value = String(context.state.values()[thumb.index()]) ?? "";
};
createEffect(() => {
setValueText(thumb.index() === -1 ? "" : context.state.getThumbValueLabel(thumb.index()));
});
return (() => {
const _el$ = _tmpl$();
_el$.addEventListener("change", onChange);
spread(_el$, mergeProps({
get id() {
return fieldProps.id();
},
get name() {
return formControlContext.name();
},
get tabIndex() {
return context.state.isDisabled() ? void 0 : -1;
},
get min() {
return context.state.getThumbMinValue(thumb.index());
},
get max() {
return context.state.getThumbMaxValue(thumb.index());
},
get step() {
return context.state.step();
},
get value() {
return context.state.values()[thumb.index()];
},
get required() {
return formControlContext.isRequired();
},
get disabled() {
return formControlContext.isDisabled();
},
get readonly() {
return formControlContext.isReadOnly();
},
get style() {
return combineStyle({
...visuallyHiddenStyles
}, local.style);
},
get ["aria-orientation"]() {
return context.state.orientation();
},
get ["aria-valuetext"]() {
return valueText();
},
get ["aria-label"]() {
return fieldProps.ariaLabel();
},
get ["aria-labelledby"]() {
return fieldProps.ariaLabelledBy();
},
get ["aria-describedby"]() {
return fieldProps.ariaDescribedBy();
},
get ["aria-invalid"]() {
return formControlContext.validationState() === "invalid" || void 0;
},
get ["aria-required"]() {
return formControlContext.isRequired() || void 0;
},
get ["aria-disabled"]() {
return formControlContext.isDisabled() || void 0;
},
get ["aria-readonly"]() {
return formControlContext.isReadOnly() || void 0;
}
}, () => context.dataset(), others), false, false);
return _el$;
})();
}
function createSliderState(props) {
let dirty = false;
const mergedProps = mergeDefaultProps(
{
minValue: () => 0,
maxValue: () => 100,
step: () => 1,
minStepsBetweenThumbs: () => 0,
orientation: () => "horizontal",
isDisabled: () => false
},
props
);
const pageSize = createMemo(() => {
let calcPageSize = (mergedProps.maxValue() - mergedProps.minValue()) / 10;
calcPageSize = snapValueToStep(
calcPageSize,
0,
calcPageSize + mergedProps.step(),
mergedProps.step()
);
return Math.max(calcPageSize, mergedProps.step());
});
const defaultValue = createMemo(() => {
return mergedProps.defaultValue() ?? [mergedProps.minValue()];
});
const [values, setValues] = createControllableArraySignal({
value: () => mergedProps.value(),
defaultValue,
onChange: (values2) => mergedProps.onChange?.(values2)
});
const [isDragging, setIsDragging] = createSignal(
new Array(values().length).fill(false)
);
const [isEditables, setEditables] = createSignal(
new Array(values().length).fill(false)
);
const [focusedIndex, setFocusedIndex] = createSignal(
void 0
);
const resetValues = () => {
setValues(defaultValue());
};
const getValuePercent = (value) => {
return (value - mergedProps.minValue()) / (mergedProps.maxValue() - mergedProps.minValue());
};
const getThumbMinValue = (index) => {
return index === 0 ? props.minValue() : values()[index - 1] + props.minStepsBetweenThumbs() * props.step();
};
const getThumbMaxValue = (index) => {
return index === values().length - 1 ? props.maxValue() : values()[index + 1] - props.minStepsBetweenThumbs() * props.step();
};
const isThumbEditable = (index) => {
return isEditables()[index];
};
const setThumbEditable = (index) => {
setEditables((p) => {
p[index] = true;
return p;
});
};
const updateValue = (index, value) => {
if (mergedProps.isDisabled() || !isThumbEditable(index))
return;
const snappedValue = snapValueToStep(
value,
getThumbMinValue(index),
getThumbMaxValue(index),
mergedProps.step()
);
const nextValues = getNextSortedValues(values(), snappedValue, index);
if (!hasMinStepsBetweenValues(
nextValues,
mergedProps.minStepsBetweenThumbs() * mergedProps.step()
)) {
return;
}
setValues((prev) => [...replaceIndex(prev, index, snappedValue)]);
};
const updateDragging = (index, dragging) => {
if (mergedProps.isDisabled() || !isThumbEditable(index))
return;
const wasDragging = isDragging()[index];
setIsDragging((p) => [...replaceIndex(p, index, dragging)]);
if (wasDragging && !isDragging().some(Boolean)) {
mergedProps.onChangeEnd?.(values());
}
};
const getFormattedValue = (value) => {
return mergedProps.numberFormatter.format(value);
};
const setThumbPercent = (index, percent) => {
updateValue(index, getPercentValue(percent));
};
const getRoundedValue = (value) => {
return Math.round((value - mergedProps.minValue()) / mergedProps.step()) * mergedProps.step() + mergedProps.minValue();
};
const getPercentValue = (percent) => {
const val = percent * (mergedProps.maxValue() - mergedProps.minValue()) + mergedProps.minValue();
return clamp(
getRoundedValue(val),
mergedProps.minValue(),
mergedProps.maxValue()
);
};
const snapThumbValue = (index, value) => {
const nextValue = values()[index] + value;
const nextValues = getNextSortedValues(values(), nextValue, index);
if (hasMinStepsBetweenValues(
nextValues,
mergedProps.minStepsBetweenThumbs() * mergedProps.step()
)) {
updateValue(
index,
snapValueToStep(
nextValue,
mergedProps.minValue(),
mergedProps.maxValue(),
mergedProps.step()
)
);
}
};
const incrementThumb = (index, stepSize = 1) => {
dirty = true;
snapThumbValue(index, Math.max(stepSize, props.step()));
};
const decrementThumb = (index, stepSize = 1) => {
dirty = true;
snapThumbValue(index, -Math.max(stepSize, props.step()));
};
return {
values,
getThumbValue: (index) => values()[index],
setThumbValue: updateValue,
setThumbPercent,
isThumbDragging: (index) => isDragging()[index],
setThumbDragging: updateDragging,
focusedThumb: focusedIndex,
setFocusedThumb: (index) => {
if (index === void 0 && dirty) {
dirty = false;
mergedProps.onChangeEnd?.(values());
}
setFocusedIndex(index);
},
getThumbPercent: (index) => getValuePercent(values()[index]),
getValuePercent,
getThumbValueLabel: (index) => getFormattedValue(values()[index]),
getFormattedValue,
getThumbMinValue,
getThumbMaxValue,
getPercentValue,
isThumbEditable,
setThumbEditable,
incrementThumb,
decrementThumb,
step: mergedProps.step,
pageSize,
orientation: mergedProps.orientation,
isDisabled: mergedProps.isDisabled,
setValues,
resetValues
};
}
function replaceIndex(array, index, value) {
if (array[index] === value) {
return array;
}
return [...array.slice(0, index), value, ...array.slice(index + 1)];
}
// src/slider/slider-root.tsx
function SliderRoot(props) {
let ref;
const defaultId = `slider-${createUniqueId()}`;
const mergedProps = mergeDefaultProps({
id: defaultId,
minValue: 0,
maxValue: 100,
step: 1,
minStepsBetweenThumbs: 0,
orientation: "horizontal",
disabled: false,
inverted: false,
getValueLabel: (params) => params.values.join(", ")
}, props);
const [local, formControlProps, others] = splitProps(mergedProps, ["ref", "value", "defaultValue", "onChange", "onChangeEnd", "inverted", "minValue", "maxValue", "step", "minStepsBetweenThumbs", "getValueLabel", "orientation"], FORM_CONTROL_PROP_NAMES);
const {
formControlContext
} = createFormControl(formControlProps);
const defaultFormatter = createNumberFormatter(() => ({
style: "decimal"
}));
const {
direction
} = useLocale();
const state = createSliderState({
value: () => local.value,
defaultValue: () => local.defaultValue ?? [local.minValue],
maxValue: () => local.maxValue,
minValue: () => local.minValue,
minStepsBetweenThumbs: () => local.minStepsBetweenThumbs,
isDisabled: () => formControlContext.isDisabled() ?? false,
orientation: () => local.orientation,
step: () => local.step,
numberFormatter: defaultFormatter(),
onChange: local.onChange,
onChangeEnd: local.onChangeEnd
});
const [thumbs, setThumbs] = createSignal([]);
const {
DomCollectionProvider
} = createDomCollection({
items: thumbs,
onItemsChange: setThumbs
});
createFormResetListener(() => ref, () => state.resetValues());
const isLTR = () => direction() === "ltr";
const isSlidingFromLeft = () => {
return isLTR() && !local.inverted || !isLTR() && local.inverted;
};
const isSlidingFromBottom = () => !local.inverted;
const isVertical = () => state.orientation() === "vertical";
const dataset = createMemo(() => {
return {
...formControlContext.dataset(),
"data-orientation": local.orientation
};
});
const [trackRef, setTrackRef] = createSignal();
let currentPosition = null;
const onSlideStart = (index, value) => {
state.setFocusedThumb(index);
state.setThumbDragging(index, true);
state.setThumbValue(index, value);
currentPosition = null;
};
const onSlideMove = ({
deltaX,
deltaY
}) => {
const active = state.focusedThumb();
if (active === void 0) {
return;
}
const {
width,
height
} = trackRef().getBoundingClientRect();
const size = isVertical() ? height : width;
if (currentPosition === null) {
currentPosition = state.getThumbPercent(state.focusedThumb()) * size;
}
let delta = isVertical() ? deltaY : deltaX;
if (!isVertical() && local.inverted || isVertical() && isSlidingFromBottom()) {
delta = -delta;
}
currentPosition += delta;
const percent = clamp(currentPosition / size, 0, 1);
const nextValues = getNextSortedValues(state.values(), currentPosition, active);
if (hasMinStepsBetweenValues(nextValues, local.minStepsBetweenThumbs * state.step())) {
state.setThumbPercent(state.focusedThumb(), percent);
local.onChange?.(state.values());
}
};
const onSlideEnd = () => {
const activeThumb = state.focusedThumb();
if (activeThumb !== void 0) {
state.setThumbDragging(activeThumb, false);
thumbs()[activeThumb].ref().focus();
}
};
const onHomeKeyDown = (event) => {
const focusedThumb = state.focusedThumb();
if (!formControlContext.isDisabled() && focusedThumb !== void 0) {
stopEventDefaultAndPropagation(event);
state.setThumbValue(focusedThumb, state.getThumbMinValue(focusedThumb));
}
};
const onEndKeyDown = (event) => {
const focusedThumb = state.focusedThumb();
if (!formControlContext.isDisabled() && focusedThumb !== void 0) {
stopEventDefaultAndPropagation(event);
state.setThumbValue(focusedThumb, state.getThumbMaxValue(focusedThumb));
}
};
const onStepKeyDown = (event, index) => {
if (!formControlContext.isDisabled()) {
switch (event.key) {
case "Left":
case "ArrowLeft":
case "Down":
case "ArrowDown":
stopEventDefaultAndPropagation(event);
if (!isLTR()) {
state.incrementThumb(index, event.shiftKey ? state.pageSize() : state.step());
} else {
state.decrementThumb(index, event.shiftKey ? state.pageSize() : state.step());
}
break;
case "Right":
case "ArrowRight":
case "Up":
case "ArrowUp":
stopEventDefaultAndPropagation(event);
if (!isLTR()) {
state.decrementThumb(index, event.shiftKey ? state.pageSize() : state.step());
} else {
state.incrementThumb(index, event.shiftKey ? state.pageSize() : state.step());
}
break;
case "Home":
onHomeKeyDown(event);
break;
case "End":
onEndKeyDown(event);
break;
case "PageUp":
stopEventDefaultAndPropagation(event);
state.incrementThumb(index, state.pageSize());
break;
case "PageDown":
stopEventDefaultAndPropagation(event);
state.decrementThumb(index, state.pageSize());
break;
}
}
};
const startEdge = createMemo(() => {
if (isVertical()) {
return isSlidingFromBottom() ? "bottom" : "top";
}
return isSlidingFromLeft() ? "left" : "right";
});
const endEdge = createMemo(() => {
if (isVertical()) {
return isSlidingFromBottom() ? "top" : "bottom";
}
return isSlidingFromLeft() ? "right" : "left";
});
const context = {
dataset,
state,
thumbs,
setThumbs,
onSlideStart,
onSlideMove,
onSlideEnd,
onStepKeyDown,
isSlidingFromLeft,
isSlidingFromBottom,
trackRef,
minValue: () => local.minValue,
maxValue: () => local.maxValue,
inverted: () => local.inverted,
startEdge,
endEdge,
registerTrack: (ref2) => setTrackRef(ref2),
generateId: createGenerateId(() => access(formControlProps.id)),
getValueLabel: local.getValueLabel
};
return createComponent(DomCollectionProvider, {
get children() {
return createComponent(FormControlContext.Provider, {
value: formControlContext,
get children() {
return createComponent(SliderContext.Provider, {
value: context,
get children() {
return createComponent(Polymorphic, mergeProps({
as: "div",
ref(r$) {
const _ref$ = mergeRefs((el) => ref = el, local.ref);
typeof _ref$ === "function" && _ref$(r$);
},
role: "group",
get id() {
return access(formControlProps.id);
}
}, dataset, others));
}
});
}
});
}
});
}
function SliderTrack(props) {
const context = useSliderContext();
const [local, others] = splitProps(props, ["onPointerDown", "onPointerMove", "onPointerUp"]);
const [sRect, setRect] = createSignal();
function getValueFromPointer(pointerPosition) {
const rect = sRect() || context.trackRef().getBoundingClientRect();
const input = [0, context.state.orientation() === "vertical" ? rect.height : rect.width];
let output = context.isSlidingFromLeft() ? [context.minValue(), context.maxValue()] : [context.maxValue(), context.minValue()];
if (context.state.orientation() === "vertical") {
output = context.isSlidingFromBottom() ? [context.maxValue(), context.minValue()] : [context.minValue(), context.maxValue()];
}
const value = linearScale(input, output);
setRect(rect);
return value(pointerPosition - (context.state.orientation() === "vertical" ? rect.top : rect.left));
}
let startPosition = 0;
const onPointerDown = (e) => {
callHandler(e, local.onPointerDown);
const target = e.target;
target.setPointerCapture(e.pointerId);
e.preventDefault();
const value = getValueFromPointer(context.state.orientation() === "horizontal" ? e.clientX : e.clientY);
startPosition = context.state.orientation() === "horizontal" ? e.clientX : e.clientY;
const closestIndex = getClosestValueIndex(context.state.values(), value);
context.onSlideStart?.(closestIndex, value);
};
const onPointerMove = (e) => {
callHandler(e, local.onPointerMove);
const target = e.target;
if (target.hasPointerCapture(e.pointerId)) {
context.onSlideMove?.({
deltaX: e.clientX - startPosition,
deltaY: e.clientY - startPosition
});
startPosition = context.state.orientation() === "horizontal" ? e.clientX : e.clientY;
}
};
const onPointerUp = (e) => {
callHandler(e, local.onPointerUp);
const target = e.target;
if (target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId);
setRect(void 0);
context.onSlideEnd?.();
}
};
return createComponent(Polymorphic, mergeProps({
as: "div",
ref(r$) {
const _ref$ = mergeRefs(context.registerTrack, props.ref);
typeof _ref$ === "function" && _ref$(r$);
},
onPointerDown,
onPointerMove,
onPointerUp
}, () => context.dataset(), others));
}
function SliderValueLabel(props) {
const context = useSliderContext();
return createComponent(Polymorphic, mergeProps({
as: "div"
}, () => context.dataset(), props, {
get children() {
return context.getValueLabel?.({
values: context.state.values(),
max: context.maxValue(),
min: context.minValue()
});
}
}));
}
// src/slider/index.tsx
var Slider = Object.assign(SliderRoot, {
Description: FormControlDescription,
ErrorMessage: FormControlErrorMessage,
Fill: SliderFill,
Input: SliderInput,
Label: FormControlLabel,
Thumb: SliderThumb,
Track: SliderTrack,
ValueLabel: SliderValueLabel
});
export { Slider, SliderFill, SliderInput, SliderRoot, SliderThumb, SliderTrack, SliderValueLabel, slider_exports, useSliderContext };