@zag-js/steps 1.32.0 → 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
@@ -8,6 +8,11 @@ declare const anatomy: _zag_js_anatomy.AnatomyInstance<"root" | "list" | "item"
8
8
  interface StepChangeDetails {
9
9
  step: number;
10
10
  }
11
+ interface StepInvalidDetails {
12
+ step: number;
13
+ action: "next" | "set";
14
+ targetStep?: number;
15
+ }
11
16
  interface ElementIds {
12
17
  root?: string | undefined;
13
18
  list?: string | undefined;
@@ -49,6 +54,21 @@ interface StepsProps extends DirectionProperty, CommonProperties {
49
54
  * The total number of steps
50
55
  */
51
56
  count?: number | undefined;
57
+ /**
58
+ * Whether a step is valid. Invalid steps block forward navigation in linear mode.
59
+ * @default () => true
60
+ */
61
+ isStepValid?: ((index: number) => boolean) | undefined;
62
+ /**
63
+ * Whether a step can be skipped during navigation.
64
+ * Skippable steps are bypassed when using next/prev.
65
+ * @default () => false
66
+ */
67
+ isStepSkippable?: ((index: number) => boolean) | undefined;
68
+ /**
69
+ * Called when navigation is blocked due to an invalid step.
70
+ */
71
+ onStepInvalid?: ((details: StepInvalidDetails) => void) | undefined;
52
72
  }
53
73
  type PropsWithDefault = "orientation" | "linear" | "count";
54
74
  interface PrivateContext {
@@ -76,14 +96,46 @@ interface ItemProps {
76
96
  index: number;
77
97
  }
78
98
  interface ItemState {
99
+ /**
100
+ * The index of the step
101
+ */
79
102
  index: number;
103
+ /**
104
+ * The id of the trigger element
105
+ */
80
106
  triggerId: string;
107
+ /**
108
+ * The id of the content element
109
+ */
81
110
  contentId: string;
111
+ /**
112
+ * Whether the step is the current step
113
+ */
82
114
  current: boolean;
115
+ /**
116
+ * Whether the step is completed (index < current step)
117
+ */
83
118
  completed: boolean;
119
+ /**
120
+ * Whether the step is incomplete (index > current step)
121
+ */
84
122
  incomplete: boolean;
123
+ /**
124
+ * Whether the step is the last step
125
+ */
85
126
  last: boolean;
127
+ /**
128
+ * Whether the step is the first step
129
+ */
86
130
  first: boolean;
131
+ /**
132
+ * Whether the step can be skipped (based on `isStepSkippable` callback)
133
+ */
134
+ skippable: boolean;
135
+ /**
136
+ * Lazy validation check - only evaluated when called
137
+ */
138
+ isValid: () => boolean;
87
139
  }
88
140
  interface StepsApi<T extends PropTypes = PropTypes> {
89
141
  /**
@@ -110,6 +162,14 @@ interface StepsApi<T extends PropTypes = PropTypes> {
110
162
  * Whether the stepper is completed.
111
163
  */
112
164
  isCompleted: boolean;
165
+ /**
166
+ * Check if a specific step is valid (lazy evaluation)
167
+ */
168
+ isStepValid: (index: number) => boolean;
169
+ /**
170
+ * Check if a specific step can be skipped
171
+ */
172
+ isStepSkippable: (index: number) => boolean;
113
173
  /**
114
174
  * Function to set the value of the stepper.
115
175
  */
@@ -149,4 +209,4 @@ declare const machine: _zag_js_core.Machine<StepsSchema>;
149
209
  declare const props: (keyof StepsProps)[];
150
210
  declare const splitProps: <Props extends Partial<StepsProps>>(props: Props) => [Partial<StepsProps>, Omit<Props, keyof StepsProps>];
151
211
 
152
- export { type StepsApi as Api, type ElementIds, type ItemProps, type ItemState, type StepsMachine as Machine, type StepsProps as Props, type StepsService as Service, type StepChangeDetails, anatomy, connect, machine, props, splitProps };
212
+ export { type StepsApi as Api, type ElementIds, type ItemProps, type ItemState, type StepsMachine as Machine, type StepsProps as Props, type StepsService as Service, type StepChangeDetails, type StepInvalidDetails, anatomy, connect, machine, props, splitProps };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,11 @@ declare const anatomy: _zag_js_anatomy.AnatomyInstance<"root" | "list" | "item"
8
8
  interface StepChangeDetails {
9
9
  step: number;
10
10
  }
11
+ interface StepInvalidDetails {
12
+ step: number;
13
+ action: "next" | "set";
14
+ targetStep?: number;
15
+ }
11
16
  interface ElementIds {
12
17
  root?: string | undefined;
13
18
  list?: string | undefined;
@@ -49,6 +54,21 @@ interface StepsProps extends DirectionProperty, CommonProperties {
49
54
  * The total number of steps
50
55
  */
51
56
  count?: number | undefined;
57
+ /**
58
+ * Whether a step is valid. Invalid steps block forward navigation in linear mode.
59
+ * @default () => true
60
+ */
61
+ isStepValid?: ((index: number) => boolean) | undefined;
62
+ /**
63
+ * Whether a step can be skipped during navigation.
64
+ * Skippable steps are bypassed when using next/prev.
65
+ * @default () => false
66
+ */
67
+ isStepSkippable?: ((index: number) => boolean) | undefined;
68
+ /**
69
+ * Called when navigation is blocked due to an invalid step.
70
+ */
71
+ onStepInvalid?: ((details: StepInvalidDetails) => void) | undefined;
52
72
  }
53
73
  type PropsWithDefault = "orientation" | "linear" | "count";
54
74
  interface PrivateContext {
@@ -76,14 +96,46 @@ interface ItemProps {
76
96
  index: number;
77
97
  }
78
98
  interface ItemState {
99
+ /**
100
+ * The index of the step
101
+ */
79
102
  index: number;
103
+ /**
104
+ * The id of the trigger element
105
+ */
80
106
  triggerId: string;
107
+ /**
108
+ * The id of the content element
109
+ */
81
110
  contentId: string;
111
+ /**
112
+ * Whether the step is the current step
113
+ */
82
114
  current: boolean;
115
+ /**
116
+ * Whether the step is completed (index < current step)
117
+ */
83
118
  completed: boolean;
119
+ /**
120
+ * Whether the step is incomplete (index > current step)
121
+ */
84
122
  incomplete: boolean;
123
+ /**
124
+ * Whether the step is the last step
125
+ */
85
126
  last: boolean;
127
+ /**
128
+ * Whether the step is the first step
129
+ */
86
130
  first: boolean;
131
+ /**
132
+ * Whether the step can be skipped (based on `isStepSkippable` callback)
133
+ */
134
+ skippable: boolean;
135
+ /**
136
+ * Lazy validation check - only evaluated when called
137
+ */
138
+ isValid: () => boolean;
87
139
  }
88
140
  interface StepsApi<T extends PropTypes = PropTypes> {
89
141
  /**
@@ -110,6 +162,14 @@ interface StepsApi<T extends PropTypes = PropTypes> {
110
162
  * Whether the stepper is completed.
111
163
  */
112
164
  isCompleted: boolean;
165
+ /**
166
+ * Check if a specific step is valid (lazy evaluation)
167
+ */
168
+ isStepValid: (index: number) => boolean;
169
+ /**
170
+ * Check if a specific step can be skipped
171
+ */
172
+ isStepSkippable: (index: number) => boolean;
113
173
  /**
114
174
  * Function to set the value of the stepper.
115
175
  */
@@ -149,4 +209,4 @@ declare const machine: _zag_js_core.Machine<StepsSchema>;
149
209
  declare const props: (keyof StepsProps)[];
150
210
  declare const splitProps: <Props extends Partial<StepsProps>>(props: Props) => [Partial<StepsProps>, Omit<Props, keyof StepsProps>];
151
211
 
152
- export { type StepsApi as Api, type ElementIds, type ItemProps, type ItemState, type StepsMachine as Machine, type StepsProps as Props, type StepsService as Service, type StepChangeDetails, anatomy, connect, machine, props, splitProps };
212
+ export { type StepsApi as Api, type ElementIds, type ItemProps, type ItemState, type StepsMachine as Machine, type StepsProps as Props, type StepsService as Service, type StepChangeDetails, type StepInvalidDetails, anatomy, connect, machine, props, splitProps };
package/dist/index.js CHANGED
@@ -35,6 +35,12 @@ function connect(service, normalize) {
35
35
  const percent = computed("percent");
36
36
  const hasNextStep = computed("hasNextStep");
37
37
  const hasPrevStep = computed("hasPrevStep");
38
+ const isStepValid = (index) => {
39
+ return prop("isStepValid")?.(index) ?? true;
40
+ };
41
+ const isStepSkippable = (index) => {
42
+ return prop("isStepSkippable")?.(index) ?? false;
43
+ };
38
44
  const getItemState = (props2) => ({
39
45
  triggerId: getTriggerId(scope, props2.index),
40
46
  contentId: getContentId(scope, props2.index),
@@ -43,7 +49,9 @@ function connect(service, normalize) {
43
49
  incomplete: props2.index > step,
44
50
  index: props2.index,
45
51
  first: props2.index === 0,
46
- last: props2.index === count - 1
52
+ last: props2.index === count - 1,
53
+ skippable: isStepSkippable(props2.index),
54
+ isValid: () => isStepValid(props2.index)
47
55
  });
48
56
  const goToNextStep = () => {
49
57
  send({ type: "STEP.NEXT", src: "next.trigger.click" });
@@ -64,6 +72,8 @@ function connect(service, normalize) {
64
72
  hasNextStep,
65
73
  hasPrevStep,
66
74
  isCompleted: computed("completed"),
75
+ isStepValid,
76
+ isStepSkippable,
67
77
  goToNextStep,
68
78
  goToPrevStep,
69
79
  resetStep,
@@ -99,7 +109,8 @@ function connect(service, normalize) {
99
109
  ...parts.item.attrs,
100
110
  dir: prop("dir"),
101
111
  "aria-current": itemState.current ? "step" : void 0,
102
- "data-orientation": prop("orientation")
112
+ "data-orientation": prop("orientation"),
113
+ "data-skippable": domQuery.dataAttr(itemState.skippable)
103
114
  });
104
115
  },
105
116
  getTriggerProps(props2) {
@@ -233,16 +244,28 @@ var machine = core.createMachine({
233
244
  initialState() {
234
245
  return "idle";
235
246
  },
236
- entry: ["validateStep"],
247
+ entry: ["validateStepIndex"],
237
248
  states: {
238
249
  idle: {
239
250
  on: {
240
- "STEP.SET": {
241
- actions: ["setStep"]
242
- },
243
- "STEP.NEXT": {
244
- actions: ["goToNextStep"]
245
- },
251
+ "STEP.SET": [
252
+ {
253
+ guard: "isValidStepNavigation",
254
+ actions: ["setStep"]
255
+ },
256
+ {
257
+ actions: ["invokeOnStepInvalid"]
258
+ }
259
+ ],
260
+ "STEP.NEXT": [
261
+ {
262
+ guard: "isCurrentStepValid",
263
+ actions: ["goToNextStep"]
264
+ },
265
+ {
266
+ actions: ["invokeOnStepInvalid"]
267
+ }
268
+ ],
246
269
  "STEP.PREV": {
247
270
  actions: ["goToPrevStep"]
248
271
  },
@@ -253,14 +276,30 @@ var machine = core.createMachine({
253
276
  }
254
277
  },
255
278
  implementations: {
279
+ guards: {
280
+ isCurrentStepValid({ context, prop }) {
281
+ const current = context.get("step");
282
+ if (prop("isStepSkippable")?.(current)) return true;
283
+ const isStepValid = prop("isStepValid");
284
+ if (!isStepValid) return true;
285
+ return isStepValid(current);
286
+ },
287
+ isValidStepNavigation({ context, event, prop }) {
288
+ const current = context.get("step");
289
+ if (event.value <= current) return true;
290
+ if (prop("isStepSkippable")?.(current)) return true;
291
+ const isStepValid = prop("isStepValid");
292
+ if (!isStepValid) return true;
293
+ return isStepValid(current);
294
+ }
295
+ },
256
296
  actions: {
257
297
  goToNextStep({ context, prop }) {
258
- const value = Math.min(context.get("step") + 1, prop("count"));
259
- context.set("step", value);
298
+ const count = prop("count");
299
+ context.set("step", Math.min(context.get("step") + 1, count));
260
300
  },
261
301
  goToPrevStep({ context }) {
262
- const value = Math.max(context.get("step") - 1, 0);
263
- context.set("step", value);
302
+ context.set("step", Math.max(context.get("step") - 1, 0));
264
303
  },
265
304
  resetStep({ context }) {
266
305
  context.set("step", 0);
@@ -268,29 +307,39 @@ var machine = core.createMachine({
268
307
  setStep({ context, event }) {
269
308
  context.set("step", event.value);
270
309
  },
271
- validateStep({ context, prop }) {
272
- validateStep(prop("count"), context.get("step"));
310
+ validateStepIndex({ context, prop }) {
311
+ validateStepIndex(prop("count"), context.get("step"));
312
+ },
313
+ invokeOnStepInvalid({ context, event, prop }) {
314
+ prop("onStepInvalid")?.({
315
+ step: context.get("step"),
316
+ action: event.type === "STEP.NEXT" ? "next" : "set",
317
+ targetStep: event.value
318
+ });
273
319
  }
274
320
  }
275
321
  }
276
322
  });
277
- var validateStep = (count, step) => {
323
+ var validateStepIndex = (count, step) => {
278
324
  if (!utils.isValueWithinRange(step, 0, count)) {
279
325
  throw new RangeError(`[zag-js/steps] step index ${step} is out of bounds`);
280
326
  }
281
327
  };
282
328
  var props = types.createProps()([
283
329
  "count",
330
+ "defaultStep",
284
331
  "dir",
285
332
  "getRootNode",
286
333
  "id",
287
334
  "ids",
335
+ "isStepSkippable",
336
+ "isStepValid",
288
337
  "linear",
289
338
  "onStepChange",
290
339
  "onStepComplete",
340
+ "onStepInvalid",
291
341
  "orientation",
292
- "step",
293
- "defaultStep"
342
+ "step"
294
343
  ]);
295
344
  var splitProps = utils.createSplitProps(props);
296
345
 
package/dist/index.mjs CHANGED
@@ -33,6 +33,12 @@ function connect(service, normalize) {
33
33
  const percent = computed("percent");
34
34
  const hasNextStep = computed("hasNextStep");
35
35
  const hasPrevStep = computed("hasPrevStep");
36
+ const isStepValid = (index) => {
37
+ return prop("isStepValid")?.(index) ?? true;
38
+ };
39
+ const isStepSkippable = (index) => {
40
+ return prop("isStepSkippable")?.(index) ?? false;
41
+ };
36
42
  const getItemState = (props2) => ({
37
43
  triggerId: getTriggerId(scope, props2.index),
38
44
  contentId: getContentId(scope, props2.index),
@@ -41,7 +47,9 @@ function connect(service, normalize) {
41
47
  incomplete: props2.index > step,
42
48
  index: props2.index,
43
49
  first: props2.index === 0,
44
- last: props2.index === count - 1
50
+ last: props2.index === count - 1,
51
+ skippable: isStepSkippable(props2.index),
52
+ isValid: () => isStepValid(props2.index)
45
53
  });
46
54
  const goToNextStep = () => {
47
55
  send({ type: "STEP.NEXT", src: "next.trigger.click" });
@@ -62,6 +70,8 @@ function connect(service, normalize) {
62
70
  hasNextStep,
63
71
  hasPrevStep,
64
72
  isCompleted: computed("completed"),
73
+ isStepValid,
74
+ isStepSkippable,
65
75
  goToNextStep,
66
76
  goToPrevStep,
67
77
  resetStep,
@@ -97,7 +107,8 @@ function connect(service, normalize) {
97
107
  ...parts.item.attrs,
98
108
  dir: prop("dir"),
99
109
  "aria-current": itemState.current ? "step" : void 0,
100
- "data-orientation": prop("orientation")
110
+ "data-orientation": prop("orientation"),
111
+ "data-skippable": dataAttr(itemState.skippable)
101
112
  });
102
113
  },
103
114
  getTriggerProps(props2) {
@@ -231,16 +242,28 @@ var machine = createMachine({
231
242
  initialState() {
232
243
  return "idle";
233
244
  },
234
- entry: ["validateStep"],
245
+ entry: ["validateStepIndex"],
235
246
  states: {
236
247
  idle: {
237
248
  on: {
238
- "STEP.SET": {
239
- actions: ["setStep"]
240
- },
241
- "STEP.NEXT": {
242
- actions: ["goToNextStep"]
243
- },
249
+ "STEP.SET": [
250
+ {
251
+ guard: "isValidStepNavigation",
252
+ actions: ["setStep"]
253
+ },
254
+ {
255
+ actions: ["invokeOnStepInvalid"]
256
+ }
257
+ ],
258
+ "STEP.NEXT": [
259
+ {
260
+ guard: "isCurrentStepValid",
261
+ actions: ["goToNextStep"]
262
+ },
263
+ {
264
+ actions: ["invokeOnStepInvalid"]
265
+ }
266
+ ],
244
267
  "STEP.PREV": {
245
268
  actions: ["goToPrevStep"]
246
269
  },
@@ -251,14 +274,30 @@ var machine = createMachine({
251
274
  }
252
275
  },
253
276
  implementations: {
277
+ guards: {
278
+ isCurrentStepValid({ context, prop }) {
279
+ const current = context.get("step");
280
+ if (prop("isStepSkippable")?.(current)) return true;
281
+ const isStepValid = prop("isStepValid");
282
+ if (!isStepValid) return true;
283
+ return isStepValid(current);
284
+ },
285
+ isValidStepNavigation({ context, event, prop }) {
286
+ const current = context.get("step");
287
+ if (event.value <= current) return true;
288
+ if (prop("isStepSkippable")?.(current)) return true;
289
+ const isStepValid = prop("isStepValid");
290
+ if (!isStepValid) return true;
291
+ return isStepValid(current);
292
+ }
293
+ },
254
294
  actions: {
255
295
  goToNextStep({ context, prop }) {
256
- const value = Math.min(context.get("step") + 1, prop("count"));
257
- context.set("step", value);
296
+ const count = prop("count");
297
+ context.set("step", Math.min(context.get("step") + 1, count));
258
298
  },
259
299
  goToPrevStep({ context }) {
260
- const value = Math.max(context.get("step") - 1, 0);
261
- context.set("step", value);
300
+ context.set("step", Math.max(context.get("step") - 1, 0));
262
301
  },
263
302
  resetStep({ context }) {
264
303
  context.set("step", 0);
@@ -266,29 +305,39 @@ var machine = createMachine({
266
305
  setStep({ context, event }) {
267
306
  context.set("step", event.value);
268
307
  },
269
- validateStep({ context, prop }) {
270
- validateStep(prop("count"), context.get("step"));
308
+ validateStepIndex({ context, prop }) {
309
+ validateStepIndex(prop("count"), context.get("step"));
310
+ },
311
+ invokeOnStepInvalid({ context, event, prop }) {
312
+ prop("onStepInvalid")?.({
313
+ step: context.get("step"),
314
+ action: event.type === "STEP.NEXT" ? "next" : "set",
315
+ targetStep: event.value
316
+ });
271
317
  }
272
318
  }
273
319
  }
274
320
  });
275
- var validateStep = (count, step) => {
321
+ var validateStepIndex = (count, step) => {
276
322
  if (!isValueWithinRange(step, 0, count)) {
277
323
  throw new RangeError(`[zag-js/steps] step index ${step} is out of bounds`);
278
324
  }
279
325
  };
280
326
  var props = createProps()([
281
327
  "count",
328
+ "defaultStep",
282
329
  "dir",
283
330
  "getRootNode",
284
331
  "id",
285
332
  "ids",
333
+ "isStepSkippable",
334
+ "isStepValid",
286
335
  "linear",
287
336
  "onStepChange",
288
337
  "onStepComplete",
338
+ "onStepInvalid",
289
339
  "orientation",
290
- "step",
291
- "defaultStep"
340
+ "step"
292
341
  ]);
293
342
  var splitProps = createSplitProps(props);
294
343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/steps",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "Core logic for the steps 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.32.0",
31
- "@zag-js/core": "1.32.0",
32
- "@zag-js/dom-query": "1.32.0",
33
- "@zag-js/utils": "1.32.0",
34
- "@zag-js/types": "1.32.0"
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"