@zag-js/slider 1.31.1 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -5,6 +5,8 @@ import { Service, EventObject, Machine } from '@zag-js/core';
5
5
 
6
6
  declare const anatomy: _zag_js_anatomy.AnatomyInstance<"root" | "label" | "thumb" | "valueText" | "track" | "range" | "control" | "markerGroup" | "marker" | "draggingIndicator">;
7
7
 
8
+ type ThumbCollisionBehavior = "none" | "push" | "swap";
9
+ type ThumbAlignment = "contain" | "center";
8
10
  interface ValueChangeDetails {
9
11
  value: number[];
10
12
  }
@@ -141,8 +143,17 @@ interface SliderProps extends DirectionProperty, CommonProperties {
141
143
  width: number;
142
144
  height: number;
143
145
  } | undefined;
146
+ /**
147
+ * Controls how thumbs behave when they collide during pointer interactions.
148
+ * - `none` (default): Thumbs cannot move past each other; excess movement is ignored.
149
+ * - `push`: Thumbs push each other without restoring their previous positions when dragged back.
150
+ * - `swap`: Thumbs swap places when dragged past each other.
151
+ *
152
+ * @default "none"
153
+ */
154
+ thumbCollisionBehavior?: "none" | "push" | "swap" | undefined;
144
155
  }
145
- type PropsWithDefault = "dir" | "min" | "max" | "step" | "orientation" | "defaultValue" | "origin" | "thumbAlignment" | "minStepsBetweenThumbs";
156
+ type PropsWithDefault = "dir" | "min" | "max" | "step" | "orientation" | "defaultValue" | "origin" | "thumbAlignment" | "minStepsBetweenThumbs" | "thumbCollisionBehavior";
146
157
  type Computed = Readonly<{
147
158
  /**
148
159
  * @computed
@@ -208,6 +219,11 @@ interface SliderSchema {
208
219
  x: number;
209
220
  y: number;
210
221
  } | null;
222
+ /**
223
+ * The values when a thumb drag starts.
224
+ * Used for swap collision behavior to determine swap direction.
225
+ */
226
+ thumbDragStartValue: number[] | null;
211
227
  };
212
228
  computed: Computed;
213
229
  event: EventObject;
@@ -316,4 +332,4 @@ declare const splitThumbProps: <Props extends ThumbProps>(props: Props) => [Thum
316
332
  declare const markerProps: "value"[];
317
333
  declare const splitMarkerProps: <Props extends MarkerProps>(props: Props) => [MarkerProps, Omit<Props, "value">];
318
334
 
319
- export { type SliderApi as Api, type DraggingIndicatorProps, type ElementIds, type FocusChangeDetails, type SliderMachine as Machine, type MarkerProps, type SliderProps as Props, type SliderService as Service, type ThumbProps, type ValueChangeDetails, type ValueTextDetails, anatomy, connect, machine, markerProps, props, splitMarkerProps, splitProps, splitThumbProps, thumbProps };
335
+ export { type SliderApi as Api, type DraggingIndicatorProps, type ElementIds, type FocusChangeDetails, type SliderMachine as Machine, type MarkerProps, type SliderProps as Props, type SliderService as Service, type ThumbAlignment, type ThumbCollisionBehavior, type ThumbProps, type ValueChangeDetails, type ValueTextDetails, anatomy, connect, machine, markerProps, props, splitMarkerProps, splitProps, splitThumbProps, thumbProps };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ import { Service, EventObject, Machine } from '@zag-js/core';
5
5
 
6
6
  declare const anatomy: _zag_js_anatomy.AnatomyInstance<"root" | "label" | "thumb" | "valueText" | "track" | "range" | "control" | "markerGroup" | "marker" | "draggingIndicator">;
7
7
 
8
+ type ThumbCollisionBehavior = "none" | "push" | "swap";
9
+ type ThumbAlignment = "contain" | "center";
8
10
  interface ValueChangeDetails {
9
11
  value: number[];
10
12
  }
@@ -141,8 +143,17 @@ interface SliderProps extends DirectionProperty, CommonProperties {
141
143
  width: number;
142
144
  height: number;
143
145
  } | undefined;
146
+ /**
147
+ * Controls how thumbs behave when they collide during pointer interactions.
148
+ * - `none` (default): Thumbs cannot move past each other; excess movement is ignored.
149
+ * - `push`: Thumbs push each other without restoring their previous positions when dragged back.
150
+ * - `swap`: Thumbs swap places when dragged past each other.
151
+ *
152
+ * @default "none"
153
+ */
154
+ thumbCollisionBehavior?: "none" | "push" | "swap" | undefined;
144
155
  }
145
- type PropsWithDefault = "dir" | "min" | "max" | "step" | "orientation" | "defaultValue" | "origin" | "thumbAlignment" | "minStepsBetweenThumbs";
156
+ type PropsWithDefault = "dir" | "min" | "max" | "step" | "orientation" | "defaultValue" | "origin" | "thumbAlignment" | "minStepsBetweenThumbs" | "thumbCollisionBehavior";
146
157
  type Computed = Readonly<{
147
158
  /**
148
159
  * @computed
@@ -208,6 +219,11 @@ interface SliderSchema {
208
219
  x: number;
209
220
  y: number;
210
221
  } | null;
222
+ /**
223
+ * The values when a thumb drag starts.
224
+ * Used for swap collision behavior to determine swap direction.
225
+ */
226
+ thumbDragStartValue: number[] | null;
211
227
  };
212
228
  computed: Computed;
213
229
  event: EventObject;
@@ -316,4 +332,4 @@ declare const splitThumbProps: <Props extends ThumbProps>(props: Props) => [Thum
316
332
  declare const markerProps: "value"[];
317
333
  declare const splitMarkerProps: <Props extends MarkerProps>(props: Props) => [MarkerProps, Omit<Props, "value">];
318
334
 
319
- export { type SliderApi as Api, type DraggingIndicatorProps, type ElementIds, type FocusChangeDetails, type SliderMachine as Machine, type MarkerProps, type SliderProps as Props, type SliderService as Service, type ThumbProps, type ValueChangeDetails, type ValueTextDetails, anatomy, connect, machine, markerProps, props, splitMarkerProps, splitProps, splitThumbProps, thumbProps };
335
+ export { type SliderApi as Api, type DraggingIndicatorProps, type ElementIds, type FocusChangeDetails, type SliderMachine as Machine, type MarkerProps, type SliderProps as Props, type SliderService as Service, type ThumbAlignment, type ThumbCollisionBehavior, type ThumbProps, type ValueChangeDetails, type ValueTextDetails, anatomy, connect, machine, markerProps, props, splitMarkerProps, splitProps, splitThumbProps, thumbProps };
package/dist/index.js CHANGED
@@ -35,8 +35,13 @@ var getThumbEls = (ctx) => domQuery.queryAll(getControlEl(ctx), "[role=slider]")
35
35
  var getFirstThumbEl = (ctx) => getThumbEls(ctx)[0];
36
36
  var getHiddenInputEl = (ctx, index) => ctx.getById(getHiddenInputId(ctx, index));
37
37
  var getControlEl = (ctx) => ctx.getById(getControlId(ctx));
38
+ var getThumbInset = (thumbSize, thumbAlignment, orientation) => {
39
+ const isContain = thumbAlignment === "contain";
40
+ const isVertical = orientation === "vertical";
41
+ return isContain ? (isVertical ? thumbSize?.height ?? 0 : thumbSize?.width ?? 0) / 2 : 0;
42
+ };
38
43
  var getPointValue = (params, point) => {
39
- const { prop, scope, refs } = params;
44
+ const { context, prop, scope, refs } = params;
40
45
  const controlEl = getControlEl(scope);
41
46
  if (!controlEl) return;
42
47
  const offset = refs.get("thumbDragOffset");
@@ -44,7 +49,8 @@ var getPointValue = (params, point) => {
44
49
  x: point.x - (offset?.x ?? 0),
45
50
  y: point.y - (offset?.y ?? 0)
46
51
  };
47
- const relativePoint = domQuery.getRelativePoint(adjustedPoint, controlEl);
52
+ const thumbInset = getThumbInset(context.get("thumbSize"), prop("thumbAlignment"), prop("orientation"));
53
+ const relativePoint = getRelativePointWithInset(adjustedPoint, controlEl, thumbInset);
48
54
  const percent = relativePoint.getPercentValue({
49
55
  orientation: prop("orientation"),
50
56
  dir: prop("dir"),
@@ -52,6 +58,31 @@ var getPointValue = (params, point) => {
52
58
  });
53
59
  return utils.getPercentValue(percent, prop("min"), prop("max"), prop("step"));
54
60
  };
61
+ function getRelativePointWithInset(point, element, inset) {
62
+ const { left, top, width, height } = element.getBoundingClientRect();
63
+ const effectiveWidth = width - inset * 2;
64
+ const effectiveHeight = height - inset * 2;
65
+ const effectiveLeft = left + inset;
66
+ const effectiveTop = top + inset;
67
+ const offset = {
68
+ x: point.x - effectiveLeft,
69
+ y: point.y - effectiveTop
70
+ };
71
+ const percent = {
72
+ x: effectiveWidth > 0 ? utils.clampPercent(offset.x / effectiveWidth) : 0,
73
+ y: effectiveHeight > 0 ? utils.clampPercent(offset.y / effectiveHeight) : 0
74
+ };
75
+ function getPercentValue3(options = {}) {
76
+ const { dir = "ltr", orientation = "horizontal", inverted } = options;
77
+ const invertX = typeof inverted === "object" ? inverted.x : inverted;
78
+ const invertY = typeof inverted === "object" ? inverted.y : inverted;
79
+ if (orientation === "horizontal") {
80
+ return dir === "rtl" || invertX ? 1 - percent.x : percent.x;
81
+ }
82
+ return invertY ? 1 - percent.y : percent.y;
83
+ }
84
+ return { offset, percent, getPercentValue: getPercentValue3 };
85
+ }
55
86
  var dispatchChangeEvent = (ctx, value) => {
56
87
  value.forEach((value2, index) => {
57
88
  const inputEl = getHiddenInputEl(ctx, index);
@@ -202,6 +233,83 @@ function getMarkerGroupStyle() {
202
233
  position: "relative"
203
234
  };
204
235
  }
236
+ function getThumbBounds(ctx) {
237
+ const { index, values, min, max, gap } = ctx;
238
+ const prevThumb = values[index - 1];
239
+ const nextThumb = values[index + 1];
240
+ return {
241
+ min: prevThumb != null ? prevThumb + gap : min,
242
+ max: nextThumb != null ? nextThumb - gap : max
243
+ };
244
+ }
245
+ function round(value) {
246
+ return Math.round(value * 1e10) / 1e10;
247
+ }
248
+ function handleNone(ctx) {
249
+ const { index, value, values } = ctx;
250
+ const bounds = getThumbBounds(ctx);
251
+ const nextValues = values.slice();
252
+ nextValues[index] = round(utils.clampValue(value, bounds.min, bounds.max));
253
+ return { values: nextValues, index, swapped: false };
254
+ }
255
+ function handlePush(ctx) {
256
+ const { index, value, values, min, max, gap } = ctx;
257
+ const nextValues = values.slice();
258
+ const absoluteMin = min + index * gap;
259
+ const absoluteMax = max - (values.length - 1 - index) * gap;
260
+ nextValues[index] = round(utils.clampValue(value, absoluteMin, absoluteMax));
261
+ for (let i = index + 1; i < values.length; i++) {
262
+ const minAllowed = nextValues[i - 1] + gap;
263
+ if (nextValues[i] < minAllowed) {
264
+ nextValues[i] = round(minAllowed);
265
+ }
266
+ }
267
+ for (let i = index - 1; i >= 0; i--) {
268
+ const maxAllowed = nextValues[i + 1] - gap;
269
+ if (nextValues[i] > maxAllowed) {
270
+ nextValues[i] = round(maxAllowed);
271
+ }
272
+ }
273
+ return { values: nextValues, index, swapped: false };
274
+ }
275
+ function handleSwap(ctx, startValue) {
276
+ const { index, value, values, gap } = ctx;
277
+ const prevThumb = values[index - 1];
278
+ const nextThumb = values[index + 1];
279
+ const crossingNext = nextThumb != null && value >= nextThumb && value > startValue;
280
+ const crossingPrev = prevThumb != null && value <= prevThumb && value < startValue;
281
+ if (!crossingNext && !crossingPrev) {
282
+ return handleNone(ctx);
283
+ }
284
+ const swapIndex = crossingNext ? index + 1 : index - 1;
285
+ const nextValues = values.slice();
286
+ const newCtx = { ...ctx, index: swapIndex };
287
+ const bounds = getThumbBounds(newCtx);
288
+ nextValues[swapIndex] = round(utils.clampValue(value, bounds.min, bounds.max));
289
+ nextValues[index] = values[swapIndex];
290
+ if (crossingNext && nextValues[index] > nextValues[swapIndex] - gap) {
291
+ nextValues[index] = round(nextValues[swapIndex] - gap);
292
+ } else if (crossingPrev && nextValues[index] < nextValues[swapIndex] + gap) {
293
+ nextValues[index] = round(nextValues[swapIndex] + gap);
294
+ }
295
+ return { values: nextValues, index: swapIndex, swapped: true };
296
+ }
297
+ function resolveThumbCollision(behavior, index, value, values, min, max, step, minStepsBetweenThumbs, startValue) {
298
+ if (values.length === 1) {
299
+ return { values: [round(utils.clampValue(value, min, max))], index: 0, swapped: false };
300
+ }
301
+ const gap = step * minStepsBetweenThumbs;
302
+ const ctx = { behavior, index, value, values, min, max, gap };
303
+ switch (behavior) {
304
+ case "push":
305
+ return handlePush(ctx);
306
+ case "swap":
307
+ return handleSwap(ctx, startValue ?? values[index]);
308
+ case "none":
309
+ default:
310
+ return handleNone(ctx);
311
+ }
312
+ }
205
313
  function normalizeValues(params, nextValues) {
206
314
  return nextValues.map((value, index) => {
207
315
  return constrainValue(params, value, index);
@@ -609,6 +717,7 @@ var machine = core.createMachine({
609
717
  thumbAlignment: "contain",
610
718
  origin: "start",
611
719
  orientation: "horizontal",
720
+ thumbCollisionBehavior: "none",
612
721
  minStepsBetweenThumbs,
613
722
  ...props2,
614
723
  defaultValue: normalize(defaultValue, min, max, step, minStepsBetweenThumbs),
@@ -651,7 +760,8 @@ var machine = core.createMachine({
651
760
  },
652
761
  refs() {
653
762
  return {
654
- thumbDragOffset: null
763
+ thumbDragOffset: null,
764
+ thumbDragStartValue: null
655
765
  };
656
766
  },
657
767
  computed: {
@@ -699,7 +809,7 @@ var machine = core.createMachine({
699
809
  on: {
700
810
  POINTER_DOWN: {
701
811
  target: "dragging",
702
- actions: ["setClosestThumbIndex", "setPointerValue", "focusActiveThumb"]
812
+ actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
703
813
  },
704
814
  FOCUS: {
705
815
  target: "focus",
@@ -707,7 +817,7 @@ var machine = core.createMachine({
707
817
  },
708
818
  THUMB_POINTER_DOWN: {
709
819
  target: "dragging",
710
- actions: ["setFocusedIndex", "setThumbDragOffset", "focusActiveThumb"]
820
+ actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
711
821
  }
712
822
  }
713
823
  },
@@ -716,11 +826,11 @@ var machine = core.createMachine({
716
826
  on: {
717
827
  POINTER_DOWN: {
718
828
  target: "dragging",
719
- actions: ["setClosestThumbIndex", "setPointerValue", "focusActiveThumb"]
829
+ actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
720
830
  },
721
831
  THUMB_POINTER_DOWN: {
722
832
  target: "dragging",
723
- actions: ["setFocusedIndex", "setThumbDragOffset", "focusActiveThumb"]
833
+ actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
724
834
  },
725
835
  ARROW_DEC: {
726
836
  actions: ["decrementThumbAtIndex", "invokeOnChangeEnd"]
@@ -746,14 +856,14 @@ var machine = core.createMachine({
746
856
  on: {
747
857
  POINTER_UP: {
748
858
  target: "focus",
749
- actions: ["invokeOnChangeEnd", "clearThumbDragOffset"]
859
+ actions: ["invokeOnChangeEnd", "clearThumbDragOffset", "clearThumbDragStartValue"]
750
860
  },
751
861
  POINTER_MOVE: {
752
862
  actions: ["setPointerValue"]
753
863
  },
754
864
  POINTER_CANCEL: {
755
865
  target: "idle",
756
- actions: ["clearFocusedIndex", "clearThumbDragOffset"]
866
+ actions: ["clearFocusedIndex", "clearThumbDragOffset", "clearThumbDragStartValue"]
757
867
  }
758
868
  }
759
869
  }
@@ -834,14 +944,34 @@ var machine = core.createMachine({
834
944
  clearThumbDragOffset({ refs }) {
835
945
  refs.set("thumbDragOffset", null);
836
946
  },
947
+ setThumbDragStartValue({ refs, context }) {
948
+ refs.set("thumbDragStartValue", context.get("value").slice());
949
+ },
950
+ clearThumbDragStartValue({ refs }) {
951
+ refs.set("thumbDragStartValue", null);
952
+ },
837
953
  setPointerValue(params) {
838
954
  queueMicrotask(() => {
839
- const { context, event } = params;
955
+ const { context, event, prop, refs } = params;
840
956
  const pointValue = getPointValue(params, event.point);
841
957
  if (pointValue == null) return;
842
958
  const focusedIndex = context.get("focusedIndex");
843
- const value = constrainValue(params, pointValue, focusedIndex);
844
- context.set("value", (prev) => utils.setValueAtIndex(prev, focusedIndex, value));
959
+ const startValues = refs.get("thumbDragStartValue");
960
+ const result = resolveThumbCollision(
961
+ prop("thumbCollisionBehavior"),
962
+ focusedIndex,
963
+ pointValue,
964
+ context.get("value"),
965
+ prop("min"),
966
+ prop("max"),
967
+ prop("step"),
968
+ prop("minStepsBetweenThumbs"),
969
+ startValues?.[focusedIndex]
970
+ );
971
+ if (result.swapped) {
972
+ context.set("focusedIndex", result.index);
973
+ }
974
+ context.set("value", result.values);
845
975
  });
846
976
  },
847
977
  focusActiveThumb({ scope, context }) {
@@ -908,7 +1038,7 @@ var props = types.createProps()([
908
1038
  "readOnly",
909
1039
  "step",
910
1040
  "thumbAlignment",
911
- "thumbAlignment",
1041
+ "thumbCollisionBehavior",
912
1042
  "thumbSize",
913
1043
  "value",
914
1044
  "defaultValue"
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createAnatomy } from '@zag-js/anatomy';
2
- import { raf, setElementValue, queryAll, resizeObserverBorderBox, trackPointerMove, trackFormControl, getRelativePoint, dispatchInputValueEvent, dataAttr, isLeftClick, isModifierKey, getEventPoint, ariaAttr, getEventStep, getEventKey } from '@zag-js/dom-query';
3
- import { setValueAtIndex, callAll, getValuePercent, isEqual, createSplitProps, snapValueToStep, clampValue, getValueRanges, getNextStepValue, getPreviousStepValue, getPercentValue, pick, isValueWithinRange, first, last, toPx, getValueTransformer } from '@zag-js/utils';
2
+ import { raf, setElementValue, queryAll, resizeObserverBorderBox, trackPointerMove, trackFormControl, dispatchInputValueEvent, dataAttr, isLeftClick, isModifierKey, getEventPoint, ariaAttr, getEventStep, getEventKey } from '@zag-js/dom-query';
3
+ import { setValueAtIndex, callAll, getValuePercent, isEqual, createSplitProps, snapValueToStep, clampValue, getValueRanges, getNextStepValue, getPreviousStepValue, getPercentValue, pick, isValueWithinRange, clampPercent, first, last, toPx, getValueTransformer } from '@zag-js/utils';
4
4
  import { createMachine, memo } from '@zag-js/core';
5
5
  import { createProps } from '@zag-js/types';
6
6
 
@@ -33,8 +33,13 @@ var getThumbEls = (ctx) => queryAll(getControlEl(ctx), "[role=slider]");
33
33
  var getFirstThumbEl = (ctx) => getThumbEls(ctx)[0];
34
34
  var getHiddenInputEl = (ctx, index) => ctx.getById(getHiddenInputId(ctx, index));
35
35
  var getControlEl = (ctx) => ctx.getById(getControlId(ctx));
36
+ var getThumbInset = (thumbSize, thumbAlignment, orientation) => {
37
+ const isContain = thumbAlignment === "contain";
38
+ const isVertical = orientation === "vertical";
39
+ return isContain ? (isVertical ? thumbSize?.height ?? 0 : thumbSize?.width ?? 0) / 2 : 0;
40
+ };
36
41
  var getPointValue = (params, point) => {
37
- const { prop, scope, refs } = params;
42
+ const { context, prop, scope, refs } = params;
38
43
  const controlEl = getControlEl(scope);
39
44
  if (!controlEl) return;
40
45
  const offset = refs.get("thumbDragOffset");
@@ -42,7 +47,8 @@ var getPointValue = (params, point) => {
42
47
  x: point.x - (offset?.x ?? 0),
43
48
  y: point.y - (offset?.y ?? 0)
44
49
  };
45
- const relativePoint = getRelativePoint(adjustedPoint, controlEl);
50
+ const thumbInset = getThumbInset(context.get("thumbSize"), prop("thumbAlignment"), prop("orientation"));
51
+ const relativePoint = getRelativePointWithInset(adjustedPoint, controlEl, thumbInset);
46
52
  const percent = relativePoint.getPercentValue({
47
53
  orientation: prop("orientation"),
48
54
  dir: prop("dir"),
@@ -50,6 +56,31 @@ var getPointValue = (params, point) => {
50
56
  });
51
57
  return getPercentValue(percent, prop("min"), prop("max"), prop("step"));
52
58
  };
59
+ function getRelativePointWithInset(point, element, inset) {
60
+ const { left, top, width, height } = element.getBoundingClientRect();
61
+ const effectiveWidth = width - inset * 2;
62
+ const effectiveHeight = height - inset * 2;
63
+ const effectiveLeft = left + inset;
64
+ const effectiveTop = top + inset;
65
+ const offset = {
66
+ x: point.x - effectiveLeft,
67
+ y: point.y - effectiveTop
68
+ };
69
+ const percent = {
70
+ x: effectiveWidth > 0 ? clampPercent(offset.x / effectiveWidth) : 0,
71
+ y: effectiveHeight > 0 ? clampPercent(offset.y / effectiveHeight) : 0
72
+ };
73
+ function getPercentValue3(options = {}) {
74
+ const { dir = "ltr", orientation = "horizontal", inverted } = options;
75
+ const invertX = typeof inverted === "object" ? inverted.x : inverted;
76
+ const invertY = typeof inverted === "object" ? inverted.y : inverted;
77
+ if (orientation === "horizontal") {
78
+ return dir === "rtl" || invertX ? 1 - percent.x : percent.x;
79
+ }
80
+ return invertY ? 1 - percent.y : percent.y;
81
+ }
82
+ return { offset, percent, getPercentValue: getPercentValue3 };
83
+ }
53
84
  var dispatchChangeEvent = (ctx, value) => {
54
85
  value.forEach((value2, index) => {
55
86
  const inputEl = getHiddenInputEl(ctx, index);
@@ -200,6 +231,83 @@ function getMarkerGroupStyle() {
200
231
  position: "relative"
201
232
  };
202
233
  }
234
+ function getThumbBounds(ctx) {
235
+ const { index, values, min, max, gap } = ctx;
236
+ const prevThumb = values[index - 1];
237
+ const nextThumb = values[index + 1];
238
+ return {
239
+ min: prevThumb != null ? prevThumb + gap : min,
240
+ max: nextThumb != null ? nextThumb - gap : max
241
+ };
242
+ }
243
+ function round(value) {
244
+ return Math.round(value * 1e10) / 1e10;
245
+ }
246
+ function handleNone(ctx) {
247
+ const { index, value, values } = ctx;
248
+ const bounds = getThumbBounds(ctx);
249
+ const nextValues = values.slice();
250
+ nextValues[index] = round(clampValue(value, bounds.min, bounds.max));
251
+ return { values: nextValues, index, swapped: false };
252
+ }
253
+ function handlePush(ctx) {
254
+ const { index, value, values, min, max, gap } = ctx;
255
+ const nextValues = values.slice();
256
+ const absoluteMin = min + index * gap;
257
+ const absoluteMax = max - (values.length - 1 - index) * gap;
258
+ nextValues[index] = round(clampValue(value, absoluteMin, absoluteMax));
259
+ for (let i = index + 1; i < values.length; i++) {
260
+ const minAllowed = nextValues[i - 1] + gap;
261
+ if (nextValues[i] < minAllowed) {
262
+ nextValues[i] = round(minAllowed);
263
+ }
264
+ }
265
+ for (let i = index - 1; i >= 0; i--) {
266
+ const maxAllowed = nextValues[i + 1] - gap;
267
+ if (nextValues[i] > maxAllowed) {
268
+ nextValues[i] = round(maxAllowed);
269
+ }
270
+ }
271
+ return { values: nextValues, index, swapped: false };
272
+ }
273
+ function handleSwap(ctx, startValue) {
274
+ const { index, value, values, gap } = ctx;
275
+ const prevThumb = values[index - 1];
276
+ const nextThumb = values[index + 1];
277
+ const crossingNext = nextThumb != null && value >= nextThumb && value > startValue;
278
+ const crossingPrev = prevThumb != null && value <= prevThumb && value < startValue;
279
+ if (!crossingNext && !crossingPrev) {
280
+ return handleNone(ctx);
281
+ }
282
+ const swapIndex = crossingNext ? index + 1 : index - 1;
283
+ const nextValues = values.slice();
284
+ const newCtx = { ...ctx, index: swapIndex };
285
+ const bounds = getThumbBounds(newCtx);
286
+ nextValues[swapIndex] = round(clampValue(value, bounds.min, bounds.max));
287
+ nextValues[index] = values[swapIndex];
288
+ if (crossingNext && nextValues[index] > nextValues[swapIndex] - gap) {
289
+ nextValues[index] = round(nextValues[swapIndex] - gap);
290
+ } else if (crossingPrev && nextValues[index] < nextValues[swapIndex] + gap) {
291
+ nextValues[index] = round(nextValues[swapIndex] + gap);
292
+ }
293
+ return { values: nextValues, index: swapIndex, swapped: true };
294
+ }
295
+ function resolveThumbCollision(behavior, index, value, values, min, max, step, minStepsBetweenThumbs, startValue) {
296
+ if (values.length === 1) {
297
+ return { values: [round(clampValue(value, min, max))], index: 0, swapped: false };
298
+ }
299
+ const gap = step * minStepsBetweenThumbs;
300
+ const ctx = { behavior, index, value, values, min, max, gap };
301
+ switch (behavior) {
302
+ case "push":
303
+ return handlePush(ctx);
304
+ case "swap":
305
+ return handleSwap(ctx, startValue ?? values[index]);
306
+ case "none":
307
+ default:
308
+ return handleNone(ctx);
309
+ }
310
+ }
203
311
  function normalizeValues(params, nextValues) {
204
312
  return nextValues.map((value, index) => {
205
313
  return constrainValue(params, value, index);
@@ -607,6 +715,7 @@ var machine = createMachine({
607
715
  thumbAlignment: "contain",
608
716
  origin: "start",
609
717
  orientation: "horizontal",
718
+ thumbCollisionBehavior: "none",
610
719
  minStepsBetweenThumbs,
611
720
  ...props2,
612
721
  defaultValue: normalize(defaultValue, min, max, step, minStepsBetweenThumbs),
@@ -649,7 +758,8 @@ var machine = createMachine({
649
758
  },
650
759
  refs() {
651
760
  return {
652
- thumbDragOffset: null
761
+ thumbDragOffset: null,
762
+ thumbDragStartValue: null
653
763
  };
654
764
  },
655
765
  computed: {
@@ -697,7 +807,7 @@ var machine = createMachine({
697
807
  on: {
698
808
  POINTER_DOWN: {
699
809
  target: "dragging",
700
- actions: ["setClosestThumbIndex", "setPointerValue", "focusActiveThumb"]
810
+ actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
701
811
  },
702
812
  FOCUS: {
703
813
  target: "focus",
@@ -705,7 +815,7 @@ var machine = createMachine({
705
815
  },
706
816
  THUMB_POINTER_DOWN: {
707
817
  target: "dragging",
708
- actions: ["setFocusedIndex", "setThumbDragOffset", "focusActiveThumb"]
818
+ actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
709
819
  }
710
820
  }
711
821
  },
@@ -714,11 +824,11 @@ var machine = createMachine({
714
824
  on: {
715
825
  POINTER_DOWN: {
716
826
  target: "dragging",
717
- actions: ["setClosestThumbIndex", "setPointerValue", "focusActiveThumb"]
827
+ actions: ["setClosestThumbIndex", "setThumbDragStartValue", "setPointerValue", "focusActiveThumb"]
718
828
  },
719
829
  THUMB_POINTER_DOWN: {
720
830
  target: "dragging",
721
- actions: ["setFocusedIndex", "setThumbDragOffset", "focusActiveThumb"]
831
+ actions: ["setFocusedIndex", "setThumbDragOffset", "setThumbDragStartValue", "focusActiveThumb"]
722
832
  },
723
833
  ARROW_DEC: {
724
834
  actions: ["decrementThumbAtIndex", "invokeOnChangeEnd"]
@@ -744,14 +854,14 @@ var machine = createMachine({
744
854
  on: {
745
855
  POINTER_UP: {
746
856
  target: "focus",
747
- actions: ["invokeOnChangeEnd", "clearThumbDragOffset"]
857
+ actions: ["invokeOnChangeEnd", "clearThumbDragOffset", "clearThumbDragStartValue"]
748
858
  },
749
859
  POINTER_MOVE: {
750
860
  actions: ["setPointerValue"]
751
861
  },
752
862
  POINTER_CANCEL: {
753
863
  target: "idle",
754
- actions: ["clearFocusedIndex", "clearThumbDragOffset"]
864
+ actions: ["clearFocusedIndex", "clearThumbDragOffset", "clearThumbDragStartValue"]
755
865
  }
756
866
  }
757
867
  }
@@ -832,14 +942,34 @@ var machine = createMachine({
832
942
  clearThumbDragOffset({ refs }) {
833
943
  refs.set("thumbDragOffset", null);
834
944
  },
945
+ setThumbDragStartValue({ refs, context }) {
946
+ refs.set("thumbDragStartValue", context.get("value").slice());
947
+ },
948
+ clearThumbDragStartValue({ refs }) {
949
+ refs.set("thumbDragStartValue", null);
950
+ },
835
951
  setPointerValue(params) {
836
952
  queueMicrotask(() => {
837
- const { context, event } = params;
953
+ const { context, event, prop, refs } = params;
838
954
  const pointValue = getPointValue(params, event.point);
839
955
  if (pointValue == null) return;
840
956
  const focusedIndex = context.get("focusedIndex");
841
- const value = constrainValue(params, pointValue, focusedIndex);
842
- context.set("value", (prev) => setValueAtIndex(prev, focusedIndex, value));
957
+ const startValues = refs.get("thumbDragStartValue");
958
+ const result = resolveThumbCollision(
959
+ prop("thumbCollisionBehavior"),
960
+ focusedIndex,
961
+ pointValue,
962
+ context.get("value"),
963
+ prop("min"),
964
+ prop("max"),
965
+ prop("step"),
966
+ prop("minStepsBetweenThumbs"),
967
+ startValues?.[focusedIndex]
968
+ );
969
+ if (result.swapped) {
970
+ context.set("focusedIndex", result.index);
971
+ }
972
+ context.set("value", result.values);
843
973
  });
844
974
  },
845
975
  focusActiveThumb({ scope, context }) {
@@ -906,7 +1036,7 @@ var props = createProps()([
906
1036
  "readOnly",
907
1037
  "step",
908
1038
  "thumbAlignment",
909
- "thumbAlignment",
1039
+ "thumbCollisionBehavior",
910
1040
  "thumbSize",
911
1041
  "value",
912
1042
  "defaultValue"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/slider",
3
- "version": "1.31.1",
3
+ "version": "1.33.0",
4
4
  "description": "Core logic for the slider widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -27,11 +27,11 @@
27
27
  "url": "https://github.com/chakra-ui/zag/issues"
28
28
  },
29
29
  "dependencies": {
30
- "@zag-js/anatomy": "1.31.1",
31
- "@zag-js/core": "1.31.1",
32
- "@zag-js/dom-query": "1.31.1",
33
- "@zag-js/utils": "1.31.1",
34
- "@zag-js/types": "1.31.1"
30
+ "@zag-js/anatomy": "1.33.0",
31
+ "@zag-js/core": "1.33.0",
32
+ "@zag-js/dom-query": "1.33.0",
33
+ "@zag-js/utils": "1.33.0",
34
+ "@zag-js/types": "1.33.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "clean-package": "2.2.0"