@zag-js/slider
Version:
Core logic for the slider widget implemented as a state machine
372 lines (370 loc) • 13.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/slider.machine.ts
var slider_machine_exports = {};
__export(slider_machine_exports, {
machine: () => machine
});
module.exports = __toCommonJS(slider_machine_exports);
var import_core = require("@zag-js/core");
var import_dom_query = require("@zag-js/dom-query");
var import_utils = require("@zag-js/utils");
var dom = __toESM(require("./slider.dom.js"));
var import_slider = require("./slider.utils.js");
var isEqualSize = (a, b) => {
return a?.width === b?.width && a?.height === b?.height;
};
var normalize = (value, min, max, step, minStepsBetweenThumbs) => {
const ranges = (0, import_utils.getValueRanges)(value, min, max, minStepsBetweenThumbs * step);
return ranges.map((range) => {
const snapValue = (0, import_utils.snapValueToStep)(range.value, range.min, range.max, step);
const rangeValue = (0, import_utils.clampValue)(snapValue, range.min, range.max);
if (!(0, import_utils.isValueWithinRange)(rangeValue, min, max)) {
throw new Error(
"[zag-js/slider] The configured `min`, `max`, `step` or `minStepsBetweenThumbs` values are invalid"
);
}
return rangeValue;
});
};
var machine = (0, import_core.createMachine)({
props({ props }) {
const min = props.min ?? 0;
const max = props.max ?? 100;
const step = props.step ?? 1;
const defaultValue = props.defaultValue ?? [min];
const minStepsBetweenThumbs = props.minStepsBetweenThumbs ?? 0;
return {
dir: "ltr",
thumbAlignment: "contain",
origin: "start",
orientation: "horizontal",
thumbCollisionBehavior: "none",
minStepsBetweenThumbs,
...props,
largeStep: props.largeStep ?? 10 * step,
defaultValue: normalize(defaultValue, min, max, step, minStepsBetweenThumbs),
value: props.value ? normalize(props.value, min, max, step, minStepsBetweenThumbs) : void 0,
max,
step,
min
};
},
initialState() {
return "idle";
},
context({ prop, bindable, getContext }) {
return {
thumbSize: bindable(() => ({
defaultValue: prop("thumbSize") || null
})),
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
isEqual: import_utils.isEqual,
hash(a) {
return a.join(",");
},
onChange(value) {
prop("onValueChange")?.({ value });
}
})),
focusedIndex: bindable(() => ({
defaultValue: -1,
onChange(value) {
const ctx = getContext();
prop("onFocusChange")?.({ focusedIndex: value, value: ctx.get("value") });
}
})),
fieldsetDisabled: bindable(() => ({
defaultValue: false
}))
};
},
refs() {
return {
thumbDragOffset: null,
thumbDragStartValue: null
};
},
computed: {
isHorizontal: ({ prop }) => prop("orientation") === "horizontal",
isVertical: ({ prop }) => prop("orientation") === "vertical",
isRtl: ({ prop }) => prop("orientation") === "horizontal" && prop("dir") === "rtl",
isDisabled: ({ context, prop }) => !!prop("disabled") || context.get("fieldsetDisabled"),
isInteractive: ({ prop, computed }) => !(prop("readOnly") || computed("isDisabled")),
hasMeasuredThumbSize: ({ context }) => context.get("thumbSize") != null,
valuePercent: (0, import_core.memo)(
({ context, prop }) => [context.get("value"), prop("min"), prop("max")],
([value, min, max]) => value.map((value2) => 100 * (0, import_utils.getValuePercent)(value2, min, max))
)
},
watch({ track, action, context, computed, send }) {
track([() => context.hash("value")], () => {
action(["syncInputElements", "dispatchChangeEvent"]);
});
track([() => computed("isDisabled")], () => {
if (computed("isDisabled")) {
send({ type: "POINTER_CANCEL" });
}
});
},
effects: ["trackFormControlState", "trackThumbSize"],
on: {
SET_VALUE: [
{
guard: "hasIndex",
actions: ["setValueAtIndex", "invokeOnChangeEnd"]
},
{
actions: ["setValue", "invokeOnChangeEnd"]
}
],
INCREMENT: {
actions: ["incrementThumbAtIndex", "invokeOnChangeEnd"]
},
DECREMENT: {
actions: ["decrementThumbAtIndex", "invokeOnChangeEnd"]
}
},
states: {
idle: {
on: {
POINTER_DOWN: {
target: "dragging",
actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
},
FOCUS: {
target: "focus",
actions: ["setFocusedIndex"]
},
THUMB_POINTER_DOWN: {
target: "dragging",
actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
}
}
},
focus: {
entry: ["focusActiveThumb"],
on: {
POINTER_DOWN: {
target: "dragging",
actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
},
THUMB_POINTER_DOWN: {
target: "dragging",
actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
},
ARROW_DEC: {
actions: ["decrementThumbAtIndex", "invokeOnChangeEnd"]
},
ARROW_INC: {
actions: ["incrementThumbAtIndex", "invokeOnChangeEnd"]
},
HOME: {
actions: ["setFocusedThumbToMin", "invokeOnChangeEnd"]
},
END: {
actions: ["setFocusedThumbToMax", "invokeOnChangeEnd"]
},
BLUR: {
target: "idle",
actions: ["clearFocusedIndex"]
}
}
},
dragging: {
entry: ["focusActiveThumb"],
effects: ["trackPointerMove"],
on: {
POINTER_UP: {
target: "focus",
actions: ["invokeOnChangeEnd", "clearThumbDragOffset", "clearThumbDragStartValue"]
},
POINTER_MOVE: {
actions: ["setPointerValue"]
},
POINTER_CANCEL: {
target: "idle",
actions: ["clearFocusedIndex", "clearThumbDragOffset", "clearThumbDragStartValue"]
}
}
}
},
implementations: {
guards: {
hasIndex: ({ event }) => event.index != null
},
effects: {
trackFormControlState({ context, scope }) {
return (0, import_dom_query.trackFormControl)(dom.getRootEl(scope), {
onFieldsetDisabledChange(disabled) {
context.set("fieldsetDisabled", disabled);
},
onFormReset() {
context.set("value", context.initial("value"));
}
});
},
trackPointerMove({ scope, send }) {
return (0, import_dom_query.trackPointerMove)(scope.getDoc(), {
onPointerMove(info) {
send({ type: "POINTER_MOVE", point: info.point });
},
onPointerUp() {
send({ type: "POINTER_UP" });
}
});
},
trackThumbSize({ context, scope, prop }) {
if (prop("thumbAlignment") !== "contain" || prop("thumbSize")) return;
const exec = (el) => {
const rect = dom.getOffsetRect(el);
const size = (0, import_utils.pick)(rect, ["width", "height"]);
if (isEqualSize(context.get("thumbSize"), size)) return;
context.set("thumbSize", size);
};
const thumbEls = dom.getThumbEls(scope);
thumbEls.forEach(exec);
const cleanups = thumbEls.map((el) => import_dom_query.resizeObserverBorderBox.observe(el, () => exec(el)));
return (0, import_utils.callAll)(...cleanups);
}
},
actions: {
dispatchChangeEvent({ context, scope }) {
dom.dispatchChangeEvent(scope, context.get("value"));
},
syncInputElements({ context, scope }) {
context.get("value").forEach((value, index) => {
const inputEl = dom.getHiddenInputEl(scope, index);
(0, import_dom_query.setElementValue)(inputEl, value.toString());
});
},
invokeOnChangeEnd({ prop, context }) {
queueMicrotask(() => {
prop("onValueChangeEnd")?.({ value: context.get("value") });
});
},
setClosestThumbIndex(params) {
const { context, event } = params;
const pointValue = dom.getPointValue(params, event.point);
if (pointValue == null) return;
const focusedIndex = (0, import_slider.getClosestIndex)(params, pointValue);
context.set("focusedIndex", focusedIndex);
},
setFocusedIndex(params) {
const { context, event } = params;
const movableIndex = (0, import_slider.selectMovableThumb)(params, event.index);
context.set("focusedIndex", movableIndex);
},
clearFocusedIndex({ context }) {
context.set("focusedIndex", -1);
},
setThumbDragOffset(params) {
const { refs, event } = params;
refs.set("thumbDragOffset", event.offset ?? null);
},
clearThumbDragOffset({ refs }) {
refs.set("thumbDragOffset", null);
},
setThumbDragStartValue({ refs, context }) {
refs.set("thumbDragStartValue", context.get("value").slice());
},
clearThumbDragStartValue({ refs }) {
refs.set("thumbDragStartValue", null);
},
setPointerValue(params) {
queueMicrotask(() => {
const { context, event, prop, refs } = params;
const pointValue = dom.getPointValue(params, event.point);
if (pointValue == null) return;
const focusedIndex = context.get("focusedIndex");
const startValues = refs.get("thumbDragStartValue");
const result = (0, import_slider.resolveThumbCollision)(
prop("thumbCollisionBehavior"),
focusedIndex,
pointValue,
context.get("value"),
prop("min"),
prop("max"),
prop("step"),
prop("minStepsBetweenThumbs"),
startValues?.[focusedIndex]
);
if (result.swapped) {
context.set("focusedIndex", result.index);
}
context.set("value", result.values);
});
},
focusActiveThumb({ scope, context }) {
(0, import_dom_query.raf)(() => {
const thumbEl = dom.getThumbEl(scope, context.get("focusedIndex"));
thumbEl?.focus({ preventScroll: true });
});
},
decrementThumbAtIndex(params) {
const { context, event } = params;
const value = (0, import_slider.decrement)(params, event.index, event.step);
context.set("value", value);
},
incrementThumbAtIndex(params) {
const { context, event } = params;
const value = (0, import_slider.increment)(params, event.index, event.step);
context.set("value", value);
},
setFocusedThumbToMin(params) {
const { context } = params;
const index = context.get("focusedIndex");
const { min } = (0, import_slider.getRangeAtIndex)(params, index);
context.set("value", (prev) => (0, import_utils.setValueAtIndex)(prev, index, min));
},
setFocusedThumbToMax(params) {
const { context } = params;
const index = context.get("focusedIndex");
const { max } = (0, import_slider.getRangeAtIndex)(params, index);
context.set("value", (prev) => (0, import_utils.setValueAtIndex)(prev, index, max));
},
setValueAtIndex(params) {
const { context, event } = params;
const value = (0, import_slider.constrainValue)(params, event.value, event.index);
context.set("value", (prev) => (0, import_utils.setValueAtIndex)(prev, event.index, value));
},
setValue(params) {
const { context, event } = params;
const value = (0, import_slider.normalizeValues)(params, event.value);
context.set("value", value);
}
}
}
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
machine
});