@zag-js/tabs 0.7.0 → 0.9.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.
@@ -24,26 +24,31 @@ var dom = createScope({
24
24
  getActiveContentEl: (ctx) => {
25
25
  if (!ctx.value)
26
26
  return;
27
- const id = dom.getContentId(ctx, ctx.value);
28
- return dom.getById(ctx, id);
27
+ return dom.getContentEl(ctx, ctx.value);
29
28
  },
30
- getRectById: (ctx, id) => {
31
- const empty = {
32
- offsetLeft: 0,
33
- offsetTop: 0,
34
- offsetWidth: 0,
35
- offsetHeight: 0
29
+ getActiveTabEl: (ctx) => {
30
+ if (!ctx.value)
31
+ return;
32
+ return dom.getTriggerEl(ctx, ctx.value);
33
+ },
34
+ getOffsetRect: (el) => {
35
+ return {
36
+ left: el?.offsetLeft ?? 0,
37
+ top: el?.offsetTop ?? 0,
38
+ width: el?.offsetWidth ?? 0,
39
+ height: el?.offsetHeight ?? 0
36
40
  };
37
- const tab = itemById(dom.getElements(ctx), dom.getTriggerId(ctx, id)) ?? empty;
38
- if (ctx.isVertical) {
39
- return {
40
- top: `${tab.offsetTop}px`,
41
- height: `${tab.offsetHeight}px`
42
- };
43
- }
41
+ },
42
+ getRectById: (ctx, id) => {
43
+ const tab = itemById(dom.getElements(ctx), dom.getTriggerId(ctx, id));
44
+ return dom.resolveRect(dom.getOffsetRect(tab), ctx.orientation);
45
+ },
46
+ resolveRect(rect, orientation) {
47
+ const sizeProp = orientation === "vertical" ? "height" : "width";
48
+ const placementProp = orientation === "vertical" ? "top" : "left";
44
49
  return {
45
- left: `${tab.offsetLeft}px`,
46
- width: `${tab.offsetWidth}px`
50
+ [placementProp]: `${rect[placementProp]}px`,
51
+ [sizeProp]: `${rect[sizeProp]}px`
47
52
  };
48
53
  }
49
54
  });
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-S2KW2DT4.mjs";
4
4
  import {
5
5
  dom
6
- } from "./chunk-OUTOSCZW.mjs";
6
+ } from "./chunk-EU4R7SET.mjs";
7
7
 
8
8
  // src/tabs.connect.ts
9
9
  import { getEventKey } from "@zag-js/dom-event";
@@ -36,6 +36,12 @@ function connect(state, send, normalize) {
36
36
  clearValue() {
37
37
  send({ type: "CLEAR_VALUE" });
38
38
  },
39
+ /**
40
+ * Sets the indicator rect to the tab with the given id.
41
+ */
42
+ setIndicatorRect(id) {
43
+ send({ type: "SET_INDICATOR_RECT", id });
44
+ },
39
45
  rootProps: normalize.element({
40
46
  ...parts.root.attrs,
41
47
  id: dom.getRootId(state.context),
@@ -142,12 +148,12 @@ function connect(state, send, normalize) {
142
148
  ...parts.indicator.attrs,
143
149
  "data-orientation": state.context.orientation,
144
150
  style: {
145
- "--transition-duration": "200ms",
151
+ "--transition-duration": "150ms",
146
152
  "--transition-property": "left, right, top, bottom, width, height",
147
153
  position: "absolute",
148
154
  willChange: "var(--transition-property)",
149
155
  transitionProperty: "var(--transition-property)",
150
- transitionDuration: state.context.hasMeasuredRect ? "var(--transition-duration)" : "0ms",
156
+ transitionDuration: state.context.canIndicatorTransition ? "var(--transition-duration)" : "0ms",
151
157
  transitionTimingFunction: "var(--transition-timing-function)",
152
158
  ...state.context.indicatorRect
153
159
  }
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-OUTOSCZW.mjs";
3
+ } from "./chunk-EU4R7SET.mjs";
4
4
 
5
5
  // src/tabs.machine.ts
6
6
  import { createMachine, guards } from "@zag-js/core";
7
7
  import { nextTick, raf } from "@zag-js/dom-query";
8
+ import { trackElementRect } from "@zag-js/element-rect";
8
9
  import { getFocusables } from "@zag-js/tabbable";
9
10
  import { compact } from "@zag-js/utils";
10
11
  var { not } = guards;
@@ -20,8 +21,13 @@ function machine(userContext) {
20
21
  value: null,
21
22
  focusedValue: null,
22
23
  previousValues: [],
23
- indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
24
- hasMeasuredRect: false,
24
+ indicatorRect: {
25
+ left: "0px",
26
+ top: "0px",
27
+ width: "0px",
28
+ height: "0px"
29
+ },
30
+ canIndicatorTransition: false,
25
31
  isIndicatorRendered: false,
26
32
  loop: true,
27
33
  translations: {},
@@ -32,10 +38,19 @@ function machine(userContext) {
32
38
  isVertical: (ctx2) => ctx2.orientation === "vertical"
33
39
  },
34
40
  created: ["setPrevSelectedTabs"],
41
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
42
+ exit: ["cleanupObserver"],
35
43
  watch: {
36
44
  focusedValue: "invokeOnFocus",
37
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
38
- dir: ["clearMeasured", "setIndicatorRect"]
45
+ value: [
46
+ "enableIndicatorTransition",
47
+ "invokeOnChange",
48
+ "setPrevSelectedTabs",
49
+ "syncIndicatorRect",
50
+ "setContentTabIndex"
51
+ ],
52
+ dir: ["syncIndicatorRect"],
53
+ orientation: ["syncIndicatorRect"]
39
54
  },
40
55
  on: {
41
56
  SET_VALUE: {
@@ -43,17 +58,25 @@ function machine(userContext) {
43
58
  },
44
59
  CLEAR_VALUE: {
45
60
  actions: "clearValue"
61
+ },
62
+ SET_INDICATOR_RECT: {
63
+ actions: "setIndicatorRect"
46
64
  }
47
65
  },
48
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
49
66
  states: {
50
67
  idle: {
51
68
  on: {
52
- TAB_FOCUS: {
53
- guard: "selectOnFocus",
54
- target: "focused",
55
- actions: ["setFocusedValue", "setValue"]
56
- },
69
+ TAB_FOCUS: [
70
+ {
71
+ guard: "selectOnFocus",
72
+ target: "focused",
73
+ actions: ["setFocusedValue", "setValue"]
74
+ },
75
+ {
76
+ target: "focused",
77
+ actions: "setFocusedValue"
78
+ }
79
+ ],
57
80
  TAB_CLICK: {
58
81
  target: "focused",
59
82
  actions: ["setFocusedValue", "setValue"]
@@ -126,9 +149,6 @@ function machine(userContext) {
126
149
  clearValue(ctx2) {
127
150
  ctx2.value = null;
128
151
  },
129
- invokeOnDelete(ctx2, evt) {
130
- ctx2.onDelete?.({ value: evt.value });
131
- },
132
152
  focusFirstTab(ctx2) {
133
153
  raf(() => dom.getFirstEl(ctx2)?.focus());
134
154
  },
@@ -147,24 +167,9 @@ function machine(userContext) {
147
167
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
148
168
  raf(() => prev?.focus());
149
169
  },
150
- setIndicatorRect(ctx2) {
151
- nextTick(() => {
152
- if (!ctx2.isIndicatorRendered || !ctx2.value)
153
- return;
154
- ctx2.indicatorRect = dom.getRectById(ctx2, ctx2.value);
155
- if (ctx2.hasMeasuredRect)
156
- return;
157
- nextTick(() => {
158
- ctx2.hasMeasuredRect = true;
159
- });
160
- });
161
- },
162
170
  checkRenderedElements(ctx2) {
163
171
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
164
172
  },
165
- clearMeasured(ctx2) {
166
- ctx2.hasMeasuredRect = false;
167
- },
168
173
  invokeOnChange(ctx2) {
169
174
  ctx2.onChange?.({ value: ctx2.value });
170
175
  },
@@ -173,7 +178,7 @@ function machine(userContext) {
173
178
  },
174
179
  setPrevSelectedTabs(ctx2) {
175
180
  if (ctx2.value != null) {
176
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
181
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
177
182
  }
178
183
  },
179
184
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -189,13 +194,51 @@ function machine(userContext) {
189
194
  panel.setAttribute("tabindex", "0");
190
195
  }
191
196
  });
197
+ },
198
+ cleanupObserver(ctx2) {
199
+ ctx2.indicatorCleanup?.();
200
+ },
201
+ enableIndicatorTransition(ctx2) {
202
+ ctx2.canIndicatorTransition = true;
203
+ },
204
+ setIndicatorRect(ctx2, evt) {
205
+ const value = evt.id ?? ctx2.value;
206
+ if (!ctx2.isIndicatorRendered || !value)
207
+ return;
208
+ const tabEl = dom.getTriggerEl(ctx2, value);
209
+ if (!tabEl)
210
+ return;
211
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
212
+ nextTick(() => {
213
+ ctx2.canIndicatorTransition = false;
214
+ });
215
+ },
216
+ syncIndicatorRect(ctx2) {
217
+ ctx2.indicatorCleanup?.();
218
+ const value = ctx2.value;
219
+ if (!ctx2.isIndicatorRendered || !value)
220
+ return;
221
+ const tabEl = dom.getActiveTabEl(ctx2);
222
+ if (!tabEl)
223
+ return;
224
+ ctx2.indicatorCleanup = trackElementRect(tabEl, {
225
+ getRect(el) {
226
+ return dom.getOffsetRect(el);
227
+ },
228
+ onChange(rect) {
229
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
230
+ nextTick(() => {
231
+ ctx2.canIndicatorTransition = false;
232
+ });
233
+ }
234
+ });
192
235
  }
193
236
  }
194
237
  }
195
238
  );
196
239
  }
197
240
  function pushUnique(arr, value) {
198
- const newArr = arr.slice();
241
+ const newArr = Array.from(arr).slice();
199
242
  const index = newArr.indexOf(value);
200
243
  if (index > -1) {
201
244
  newArr.splice(index, 1);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { anatomy } from './tabs.anatomy.js';
2
2
  export { connect } from './tabs.connect.js';
3
3
  export { machine } from './tabs.machine.js';
4
- export { UserDefinedContext as Context } from './tabs.types.js';
4
+ export { ContentProps, UserDefinedContext as Context, TriggerProps } from './tabs.types.js';
5
5
  import '@zag-js/anatomy';
6
6
  import '@zag-js/types';
7
7
  import '@zag-js/core';
package/dist/index.js CHANGED
@@ -61,26 +61,31 @@ var dom = (0, import_dom_query.createScope)({
61
61
  getActiveContentEl: (ctx) => {
62
62
  if (!ctx.value)
63
63
  return;
64
- const id = dom.getContentId(ctx, ctx.value);
65
- return dom.getById(ctx, id);
64
+ return dom.getContentEl(ctx, ctx.value);
66
65
  },
67
- getRectById: (ctx, id) => {
68
- const empty = {
69
- offsetLeft: 0,
70
- offsetTop: 0,
71
- offsetWidth: 0,
72
- offsetHeight: 0
66
+ getActiveTabEl: (ctx) => {
67
+ if (!ctx.value)
68
+ return;
69
+ return dom.getTriggerEl(ctx, ctx.value);
70
+ },
71
+ getOffsetRect: (el) => {
72
+ return {
73
+ left: el?.offsetLeft ?? 0,
74
+ top: el?.offsetTop ?? 0,
75
+ width: el?.offsetWidth ?? 0,
76
+ height: el?.offsetHeight ?? 0
73
77
  };
74
- const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id)) ?? empty;
75
- if (ctx.isVertical) {
76
- return {
77
- top: `${tab.offsetTop}px`,
78
- height: `${tab.offsetHeight}px`
79
- };
80
- }
78
+ },
79
+ getRectById: (ctx, id) => {
80
+ const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id));
81
+ return dom.resolveRect(dom.getOffsetRect(tab), ctx.orientation);
82
+ },
83
+ resolveRect(rect, orientation) {
84
+ const sizeProp = orientation === "vertical" ? "height" : "width";
85
+ const placementProp = orientation === "vertical" ? "top" : "left";
81
86
  return {
82
- left: `${tab.offsetLeft}px`,
83
- width: `${tab.offsetWidth}px`
87
+ [placementProp]: `${rect[placementProp]}px`,
88
+ [sizeProp]: `${rect[sizeProp]}px`
84
89
  };
85
90
  }
86
91
  });
@@ -114,6 +119,12 @@ function connect(state, send, normalize) {
114
119
  clearValue() {
115
120
  send({ type: "CLEAR_VALUE" });
116
121
  },
122
+ /**
123
+ * Sets the indicator rect to the tab with the given id.
124
+ */
125
+ setIndicatorRect(id) {
126
+ send({ type: "SET_INDICATOR_RECT", id });
127
+ },
117
128
  rootProps: normalize.element({
118
129
  ...parts.root.attrs,
119
130
  id: dom.getRootId(state.context),
@@ -220,12 +231,12 @@ function connect(state, send, normalize) {
220
231
  ...parts.indicator.attrs,
221
232
  "data-orientation": state.context.orientation,
222
233
  style: {
223
- "--transition-duration": "200ms",
234
+ "--transition-duration": "150ms",
224
235
  "--transition-property": "left, right, top, bottom, width, height",
225
236
  position: "absolute",
226
237
  willChange: "var(--transition-property)",
227
238
  transitionProperty: "var(--transition-property)",
228
- transitionDuration: state.context.hasMeasuredRect ? "var(--transition-duration)" : "0ms",
239
+ transitionDuration: state.context.canIndicatorTransition ? "var(--transition-duration)" : "0ms",
229
240
  transitionTimingFunction: "var(--transition-timing-function)",
230
241
  ...state.context.indicatorRect
231
242
  }
@@ -236,6 +247,7 @@ function connect(state, send, normalize) {
236
247
  // src/tabs.machine.ts
237
248
  var import_core = require("@zag-js/core");
238
249
  var import_dom_query3 = require("@zag-js/dom-query");
250
+ var import_element_rect = require("@zag-js/element-rect");
239
251
  var import_tabbable = require("@zag-js/tabbable");
240
252
  var import_utils2 = require("@zag-js/utils");
241
253
  var { not } = import_core.guards;
@@ -251,8 +263,13 @@ function machine(userContext) {
251
263
  value: null,
252
264
  focusedValue: null,
253
265
  previousValues: [],
254
- indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
255
- hasMeasuredRect: false,
266
+ indicatorRect: {
267
+ left: "0px",
268
+ top: "0px",
269
+ width: "0px",
270
+ height: "0px"
271
+ },
272
+ canIndicatorTransition: false,
256
273
  isIndicatorRendered: false,
257
274
  loop: true,
258
275
  translations: {},
@@ -263,10 +280,19 @@ function machine(userContext) {
263
280
  isVertical: (ctx2) => ctx2.orientation === "vertical"
264
281
  },
265
282
  created: ["setPrevSelectedTabs"],
283
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
284
+ exit: ["cleanupObserver"],
266
285
  watch: {
267
286
  focusedValue: "invokeOnFocus",
268
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
269
- dir: ["clearMeasured", "setIndicatorRect"]
287
+ value: [
288
+ "enableIndicatorTransition",
289
+ "invokeOnChange",
290
+ "setPrevSelectedTabs",
291
+ "syncIndicatorRect",
292
+ "setContentTabIndex"
293
+ ],
294
+ dir: ["syncIndicatorRect"],
295
+ orientation: ["syncIndicatorRect"]
270
296
  },
271
297
  on: {
272
298
  SET_VALUE: {
@@ -274,17 +300,25 @@ function machine(userContext) {
274
300
  },
275
301
  CLEAR_VALUE: {
276
302
  actions: "clearValue"
303
+ },
304
+ SET_INDICATOR_RECT: {
305
+ actions: "setIndicatorRect"
277
306
  }
278
307
  },
279
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
280
308
  states: {
281
309
  idle: {
282
310
  on: {
283
- TAB_FOCUS: {
284
- guard: "selectOnFocus",
285
- target: "focused",
286
- actions: ["setFocusedValue", "setValue"]
287
- },
311
+ TAB_FOCUS: [
312
+ {
313
+ guard: "selectOnFocus",
314
+ target: "focused",
315
+ actions: ["setFocusedValue", "setValue"]
316
+ },
317
+ {
318
+ target: "focused",
319
+ actions: "setFocusedValue"
320
+ }
321
+ ],
288
322
  TAB_CLICK: {
289
323
  target: "focused",
290
324
  actions: ["setFocusedValue", "setValue"]
@@ -357,9 +391,6 @@ function machine(userContext) {
357
391
  clearValue(ctx2) {
358
392
  ctx2.value = null;
359
393
  },
360
- invokeOnDelete(ctx2, evt) {
361
- ctx2.onDelete?.({ value: evt.value });
362
- },
363
394
  focusFirstTab(ctx2) {
364
395
  (0, import_dom_query3.raf)(() => dom.getFirstEl(ctx2)?.focus());
365
396
  },
@@ -378,24 +409,9 @@ function machine(userContext) {
378
409
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
379
410
  (0, import_dom_query3.raf)(() => prev?.focus());
380
411
  },
381
- setIndicatorRect(ctx2) {
382
- (0, import_dom_query3.nextTick)(() => {
383
- if (!ctx2.isIndicatorRendered || !ctx2.value)
384
- return;
385
- ctx2.indicatorRect = dom.getRectById(ctx2, ctx2.value);
386
- if (ctx2.hasMeasuredRect)
387
- return;
388
- (0, import_dom_query3.nextTick)(() => {
389
- ctx2.hasMeasuredRect = true;
390
- });
391
- });
392
- },
393
412
  checkRenderedElements(ctx2) {
394
413
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
395
414
  },
396
- clearMeasured(ctx2) {
397
- ctx2.hasMeasuredRect = false;
398
- },
399
415
  invokeOnChange(ctx2) {
400
416
  ctx2.onChange?.({ value: ctx2.value });
401
417
  },
@@ -404,7 +420,7 @@ function machine(userContext) {
404
420
  },
405
421
  setPrevSelectedTabs(ctx2) {
406
422
  if (ctx2.value != null) {
407
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
423
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
408
424
  }
409
425
  },
410
426
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -420,13 +436,51 @@ function machine(userContext) {
420
436
  panel.setAttribute("tabindex", "0");
421
437
  }
422
438
  });
439
+ },
440
+ cleanupObserver(ctx2) {
441
+ ctx2.indicatorCleanup?.();
442
+ },
443
+ enableIndicatorTransition(ctx2) {
444
+ ctx2.canIndicatorTransition = true;
445
+ },
446
+ setIndicatorRect(ctx2, evt) {
447
+ const value = evt.id ?? ctx2.value;
448
+ if (!ctx2.isIndicatorRendered || !value)
449
+ return;
450
+ const tabEl = dom.getTriggerEl(ctx2, value);
451
+ if (!tabEl)
452
+ return;
453
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
454
+ (0, import_dom_query3.nextTick)(() => {
455
+ ctx2.canIndicatorTransition = false;
456
+ });
457
+ },
458
+ syncIndicatorRect(ctx2) {
459
+ ctx2.indicatorCleanup?.();
460
+ const value = ctx2.value;
461
+ if (!ctx2.isIndicatorRendered || !value)
462
+ return;
463
+ const tabEl = dom.getActiveTabEl(ctx2);
464
+ if (!tabEl)
465
+ return;
466
+ ctx2.indicatorCleanup = (0, import_element_rect.trackElementRect)(tabEl, {
467
+ getRect(el) {
468
+ return dom.getOffsetRect(el);
469
+ },
470
+ onChange(rect) {
471
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
472
+ (0, import_dom_query3.nextTick)(() => {
473
+ ctx2.canIndicatorTransition = false;
474
+ });
475
+ }
476
+ });
423
477
  }
424
478
  }
425
479
  }
426
480
  );
427
481
  }
428
482
  function pushUnique(arr, value) {
429
- const newArr = arr.slice();
483
+ const newArr = Array.from(arr).slice();
430
484
  const index = newArr.indexOf(value);
431
485
  if (index > -1) {
432
486
  newArr.splice(index, 1);
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-FPHYRUCF.mjs";
3
+ } from "./chunk-GMBCGZJ4.mjs";
4
4
  import {
5
5
  anatomy
6
6
  } from "./chunk-S2KW2DT4.mjs";
7
7
  import {
8
8
  machine
9
- } from "./chunk-PH44XJ2S.mjs";
10
- import "./chunk-OUTOSCZW.mjs";
9
+ } from "./chunk-HGL32F6R.mjs";
10
+ import "./chunk-EU4R7SET.mjs";
11
11
  export {
12
12
  anatomy,
13
13
  connect,
@@ -1,5 +1,5 @@
1
1
  import { PropTypes, NormalizeProps } from '@zag-js/types';
2
- import { State, Send, TabProps } from './tabs.types.js';
2
+ import { State, Send, TriggerProps, ContentProps } from './tabs.types.js';
3
3
  import '@zag-js/core';
4
4
 
5
5
  declare function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): {
@@ -23,13 +23,15 @@ declare function connect<T extends PropTypes>(state: State, send: Send, normaliz
23
23
  * Clears the value of the tabs.
24
24
  */
25
25
  clearValue(): void;
26
+ /**
27
+ * Sets the indicator rect to the tab with the given id.
28
+ */
29
+ setIndicatorRect(id: string | null | undefined): void;
26
30
  rootProps: T["element"];
27
31
  tablistProps: T["element"];
28
- getTriggerProps(props: TabProps): T["button"];
32
+ getTriggerProps(props: TriggerProps): T["button"];
29
33
  contentGroupProps: T["element"];
30
- getContentProps({ value }: {
31
- value: string;
32
- }): T["element"];
34
+ getContentProps({ value }: ContentProps): T["element"];
33
35
  indicatorProps: T["element"];
34
36
  };
35
37
 
@@ -57,26 +57,31 @@ var dom = (0, import_dom_query.createScope)({
57
57
  getActiveContentEl: (ctx) => {
58
58
  if (!ctx.value)
59
59
  return;
60
- const id = dom.getContentId(ctx, ctx.value);
61
- return dom.getById(ctx, id);
60
+ return dom.getContentEl(ctx, ctx.value);
62
61
  },
63
- getRectById: (ctx, id) => {
64
- const empty = {
65
- offsetLeft: 0,
66
- offsetTop: 0,
67
- offsetWidth: 0,
68
- offsetHeight: 0
62
+ getActiveTabEl: (ctx) => {
63
+ if (!ctx.value)
64
+ return;
65
+ return dom.getTriggerEl(ctx, ctx.value);
66
+ },
67
+ getOffsetRect: (el) => {
68
+ return {
69
+ left: el?.offsetLeft ?? 0,
70
+ top: el?.offsetTop ?? 0,
71
+ width: el?.offsetWidth ?? 0,
72
+ height: el?.offsetHeight ?? 0
69
73
  };
70
- const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id)) ?? empty;
71
- if (ctx.isVertical) {
72
- return {
73
- top: `${tab.offsetTop}px`,
74
- height: `${tab.offsetHeight}px`
75
- };
76
- }
74
+ },
75
+ getRectById: (ctx, id) => {
76
+ const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id));
77
+ return dom.resolveRect(dom.getOffsetRect(tab), ctx.orientation);
78
+ },
79
+ resolveRect(rect, orientation) {
80
+ const sizeProp = orientation === "vertical" ? "height" : "width";
81
+ const placementProp = orientation === "vertical" ? "top" : "left";
77
82
  return {
78
- left: `${tab.offsetLeft}px`,
79
- width: `${tab.offsetWidth}px`
83
+ [placementProp]: `${rect[placementProp]}px`,
84
+ [sizeProp]: `${rect[sizeProp]}px`
80
85
  };
81
86
  }
82
87
  });
@@ -110,6 +115,12 @@ function connect(state, send, normalize) {
110
115
  clearValue() {
111
116
  send({ type: "CLEAR_VALUE" });
112
117
  },
118
+ /**
119
+ * Sets the indicator rect to the tab with the given id.
120
+ */
121
+ setIndicatorRect(id) {
122
+ send({ type: "SET_INDICATOR_RECT", id });
123
+ },
113
124
  rootProps: normalize.element({
114
125
  ...parts.root.attrs,
115
126
  id: dom.getRootId(state.context),
@@ -216,12 +227,12 @@ function connect(state, send, normalize) {
216
227
  ...parts.indicator.attrs,
217
228
  "data-orientation": state.context.orientation,
218
229
  style: {
219
- "--transition-duration": "200ms",
230
+ "--transition-duration": "150ms",
220
231
  "--transition-property": "left, right, top, bottom, width, height",
221
232
  position: "absolute",
222
233
  willChange: "var(--transition-property)",
223
234
  transitionProperty: "var(--transition-property)",
224
- transitionDuration: state.context.hasMeasuredRect ? "var(--transition-duration)" : "0ms",
235
+ transitionDuration: state.context.canIndicatorTransition ? "var(--transition-duration)" : "0ms",
225
236
  transitionTimingFunction: "var(--transition-timing-function)",
226
237
  ...state.context.indicatorRect
227
238
  }
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-FPHYRUCF.mjs";
3
+ } from "./chunk-GMBCGZJ4.mjs";
4
4
  import "./chunk-S2KW2DT4.mjs";
5
- import "./chunk-OUTOSCZW.mjs";
5
+ import "./chunk-EU4R7SET.mjs";
6
6
  export {
7
7
  connect
8
8
  };
@@ -38,16 +38,18 @@ declare const dom: {
38
38
  getNextEl: (ctx: MachineContext, id: string) => HTMLElement;
39
39
  getPrevEl: (ctx: MachineContext, id: string) => HTMLElement | null;
40
40
  getActiveContentEl: (ctx: MachineContext) => HTMLElement | null | undefined;
41
+ getActiveTabEl: (ctx: MachineContext) => HTMLElement | null | undefined;
42
+ getOffsetRect: (el: HTMLElement | undefined) => {
43
+ left: number;
44
+ top: number;
45
+ width: number;
46
+ height: number;
47
+ };
41
48
  getRectById: (ctx: MachineContext, id: string) => {
42
- top: string;
43
- height: string;
44
- left?: undefined;
45
- width?: undefined;
46
- } | {
47
- left: string;
48
- width: string;
49
- top?: undefined;
50
- height?: undefined;
49
+ [x: string]: string;
50
+ };
51
+ resolveRect(rect: Record<"width" | "height" | "left" | "top", number>, orientation?: "horizontal" | "vertical"): {
52
+ [x: string]: string;
51
53
  };
52
54
  };
53
55
 
package/dist/tabs.dom.js CHANGED
@@ -48,26 +48,31 @@ var dom = (0, import_dom_query.createScope)({
48
48
  getActiveContentEl: (ctx) => {
49
49
  if (!ctx.value)
50
50
  return;
51
- const id = dom.getContentId(ctx, ctx.value);
52
- return dom.getById(ctx, id);
51
+ return dom.getContentEl(ctx, ctx.value);
53
52
  },
54
- getRectById: (ctx, id) => {
55
- const empty = {
56
- offsetLeft: 0,
57
- offsetTop: 0,
58
- offsetWidth: 0,
59
- offsetHeight: 0
53
+ getActiveTabEl: (ctx) => {
54
+ if (!ctx.value)
55
+ return;
56
+ return dom.getTriggerEl(ctx, ctx.value);
57
+ },
58
+ getOffsetRect: (el) => {
59
+ return {
60
+ left: el?.offsetLeft ?? 0,
61
+ top: el?.offsetTop ?? 0,
62
+ width: el?.offsetWidth ?? 0,
63
+ height: el?.offsetHeight ?? 0
60
64
  };
61
- const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id)) ?? empty;
62
- if (ctx.isVertical) {
63
- return {
64
- top: `${tab.offsetTop}px`,
65
- height: `${tab.offsetHeight}px`
66
- };
67
- }
65
+ },
66
+ getRectById: (ctx, id) => {
67
+ const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id));
68
+ return dom.resolveRect(dom.getOffsetRect(tab), ctx.orientation);
69
+ },
70
+ resolveRect(rect, orientation) {
71
+ const sizeProp = orientation === "vertical" ? "height" : "width";
72
+ const placementProp = orientation === "vertical" ? "top" : "left";
68
73
  return {
69
- left: `${tab.offsetLeft}px`,
70
- width: `${tab.offsetWidth}px`
74
+ [placementProp]: `${rect[placementProp]}px`,
75
+ [sizeProp]: `${rect[sizeProp]}px`
71
76
  };
72
77
  }
73
78
  });
package/dist/tabs.dom.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-OUTOSCZW.mjs";
3
+ } from "./chunk-EU4R7SET.mjs";
4
4
  export {
5
5
  dom
6
6
  };
@@ -25,6 +25,7 @@ __export(tabs_machine_exports, {
25
25
  module.exports = __toCommonJS(tabs_machine_exports);
26
26
  var import_core = require("@zag-js/core");
27
27
  var import_dom_query2 = require("@zag-js/dom-query");
28
+ var import_element_rect = require("@zag-js/element-rect");
28
29
  var import_tabbable = require("@zag-js/tabbable");
29
30
  var import_utils2 = require("@zag-js/utils");
30
31
 
@@ -54,26 +55,31 @@ var dom = (0, import_dom_query.createScope)({
54
55
  getActiveContentEl: (ctx) => {
55
56
  if (!ctx.value)
56
57
  return;
57
- const id = dom.getContentId(ctx, ctx.value);
58
- return dom.getById(ctx, id);
58
+ return dom.getContentEl(ctx, ctx.value);
59
59
  },
60
- getRectById: (ctx, id) => {
61
- const empty = {
62
- offsetLeft: 0,
63
- offsetTop: 0,
64
- offsetWidth: 0,
65
- offsetHeight: 0
60
+ getActiveTabEl: (ctx) => {
61
+ if (!ctx.value)
62
+ return;
63
+ return dom.getTriggerEl(ctx, ctx.value);
64
+ },
65
+ getOffsetRect: (el) => {
66
+ return {
67
+ left: el?.offsetLeft ?? 0,
68
+ top: el?.offsetTop ?? 0,
69
+ width: el?.offsetWidth ?? 0,
70
+ height: el?.offsetHeight ?? 0
66
71
  };
67
- const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id)) ?? empty;
68
- if (ctx.isVertical) {
69
- return {
70
- top: `${tab.offsetTop}px`,
71
- height: `${tab.offsetHeight}px`
72
- };
73
- }
72
+ },
73
+ getRectById: (ctx, id) => {
74
+ const tab = (0, import_dom_query.itemById)(dom.getElements(ctx), dom.getTriggerId(ctx, id));
75
+ return dom.resolveRect(dom.getOffsetRect(tab), ctx.orientation);
76
+ },
77
+ resolveRect(rect, orientation) {
78
+ const sizeProp = orientation === "vertical" ? "height" : "width";
79
+ const placementProp = orientation === "vertical" ? "top" : "left";
74
80
  return {
75
- left: `${tab.offsetLeft}px`,
76
- width: `${tab.offsetWidth}px`
81
+ [placementProp]: `${rect[placementProp]}px`,
82
+ [sizeProp]: `${rect[sizeProp]}px`
77
83
  };
78
84
  }
79
85
  });
@@ -92,8 +98,13 @@ function machine(userContext) {
92
98
  value: null,
93
99
  focusedValue: null,
94
100
  previousValues: [],
95
- indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
96
- hasMeasuredRect: false,
101
+ indicatorRect: {
102
+ left: "0px",
103
+ top: "0px",
104
+ width: "0px",
105
+ height: "0px"
106
+ },
107
+ canIndicatorTransition: false,
97
108
  isIndicatorRendered: false,
98
109
  loop: true,
99
110
  translations: {},
@@ -104,10 +115,19 @@ function machine(userContext) {
104
115
  isVertical: (ctx2) => ctx2.orientation === "vertical"
105
116
  },
106
117
  created: ["setPrevSelectedTabs"],
118
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
119
+ exit: ["cleanupObserver"],
107
120
  watch: {
108
121
  focusedValue: "invokeOnFocus",
109
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
110
- dir: ["clearMeasured", "setIndicatorRect"]
122
+ value: [
123
+ "enableIndicatorTransition",
124
+ "invokeOnChange",
125
+ "setPrevSelectedTabs",
126
+ "syncIndicatorRect",
127
+ "setContentTabIndex"
128
+ ],
129
+ dir: ["syncIndicatorRect"],
130
+ orientation: ["syncIndicatorRect"]
111
131
  },
112
132
  on: {
113
133
  SET_VALUE: {
@@ -115,17 +135,25 @@ function machine(userContext) {
115
135
  },
116
136
  CLEAR_VALUE: {
117
137
  actions: "clearValue"
138
+ },
139
+ SET_INDICATOR_RECT: {
140
+ actions: "setIndicatorRect"
118
141
  }
119
142
  },
120
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
121
143
  states: {
122
144
  idle: {
123
145
  on: {
124
- TAB_FOCUS: {
125
- guard: "selectOnFocus",
126
- target: "focused",
127
- actions: ["setFocusedValue", "setValue"]
128
- },
146
+ TAB_FOCUS: [
147
+ {
148
+ guard: "selectOnFocus",
149
+ target: "focused",
150
+ actions: ["setFocusedValue", "setValue"]
151
+ },
152
+ {
153
+ target: "focused",
154
+ actions: "setFocusedValue"
155
+ }
156
+ ],
129
157
  TAB_CLICK: {
130
158
  target: "focused",
131
159
  actions: ["setFocusedValue", "setValue"]
@@ -198,9 +226,6 @@ function machine(userContext) {
198
226
  clearValue(ctx2) {
199
227
  ctx2.value = null;
200
228
  },
201
- invokeOnDelete(ctx2, evt) {
202
- ctx2.onDelete?.({ value: evt.value });
203
- },
204
229
  focusFirstTab(ctx2) {
205
230
  (0, import_dom_query2.raf)(() => dom.getFirstEl(ctx2)?.focus());
206
231
  },
@@ -219,24 +244,9 @@ function machine(userContext) {
219
244
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
220
245
  (0, import_dom_query2.raf)(() => prev?.focus());
221
246
  },
222
- setIndicatorRect(ctx2) {
223
- (0, import_dom_query2.nextTick)(() => {
224
- if (!ctx2.isIndicatorRendered || !ctx2.value)
225
- return;
226
- ctx2.indicatorRect = dom.getRectById(ctx2, ctx2.value);
227
- if (ctx2.hasMeasuredRect)
228
- return;
229
- (0, import_dom_query2.nextTick)(() => {
230
- ctx2.hasMeasuredRect = true;
231
- });
232
- });
233
- },
234
247
  checkRenderedElements(ctx2) {
235
248
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
236
249
  },
237
- clearMeasured(ctx2) {
238
- ctx2.hasMeasuredRect = false;
239
- },
240
250
  invokeOnChange(ctx2) {
241
251
  ctx2.onChange?.({ value: ctx2.value });
242
252
  },
@@ -245,7 +255,7 @@ function machine(userContext) {
245
255
  },
246
256
  setPrevSelectedTabs(ctx2) {
247
257
  if (ctx2.value != null) {
248
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
258
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
249
259
  }
250
260
  },
251
261
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -261,13 +271,51 @@ function machine(userContext) {
261
271
  panel.setAttribute("tabindex", "0");
262
272
  }
263
273
  });
274
+ },
275
+ cleanupObserver(ctx2) {
276
+ ctx2.indicatorCleanup?.();
277
+ },
278
+ enableIndicatorTransition(ctx2) {
279
+ ctx2.canIndicatorTransition = true;
280
+ },
281
+ setIndicatorRect(ctx2, evt) {
282
+ const value = evt.id ?? ctx2.value;
283
+ if (!ctx2.isIndicatorRendered || !value)
284
+ return;
285
+ const tabEl = dom.getTriggerEl(ctx2, value);
286
+ if (!tabEl)
287
+ return;
288
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
289
+ (0, import_dom_query2.nextTick)(() => {
290
+ ctx2.canIndicatorTransition = false;
291
+ });
292
+ },
293
+ syncIndicatorRect(ctx2) {
294
+ ctx2.indicatorCleanup?.();
295
+ const value = ctx2.value;
296
+ if (!ctx2.isIndicatorRendered || !value)
297
+ return;
298
+ const tabEl = dom.getActiveTabEl(ctx2);
299
+ if (!tabEl)
300
+ return;
301
+ ctx2.indicatorCleanup = (0, import_element_rect.trackElementRect)(tabEl, {
302
+ getRect(el) {
303
+ return dom.getOffsetRect(el);
304
+ },
305
+ onChange(rect) {
306
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
307
+ (0, import_dom_query2.nextTick)(() => {
308
+ ctx2.canIndicatorTransition = false;
309
+ });
310
+ }
311
+ });
264
312
  }
265
313
  }
266
314
  }
267
315
  );
268
316
  }
269
317
  function pushUnique(arr, value) {
270
- const newArr = arr.slice();
318
+ const newArr = Array.from(arr).slice();
271
319
  const index = newArr.indexOf(value);
272
320
  if (index > -1) {
273
321
  newArr.splice(index, 1);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  machine
3
- } from "./chunk-PH44XJ2S.mjs";
4
- import "./chunk-OUTOSCZW.mjs";
3
+ } from "./chunk-HGL32F6R.mjs";
4
+ import "./chunk-EU4R7SET.mjs";
5
5
  export {
6
6
  machine
7
7
  };
@@ -12,6 +12,13 @@ type ElementIds = Partial<{
12
12
  content: string;
13
13
  indicator: string;
14
14
  }>;
15
+ type TriggerProps = {
16
+ value: string;
17
+ disabled?: boolean;
18
+ };
19
+ type ContentProps = {
20
+ value: string;
21
+ };
15
22
  type PublicContext = DirectionProperty & CommonProperties & {
16
23
  /**
17
24
  * The ids of the elements in the tabs. Useful for composition.
@@ -84,9 +91,5 @@ type MachineState = {
84
91
  };
85
92
  type State = StateMachine.State<MachineContext, MachineState>;
86
93
  type Send = StateMachine.Send<StateMachine.AnyEventObject>;
87
- type TabProps = {
88
- value: string;
89
- disabled?: boolean;
90
- };
91
94
 
92
- export { MachineContext, MachineState, Send, State, TabProps, UserDefinedContext };
95
+ export { ContentProps, MachineContext, MachineState, Send, State, TriggerProps, UserDefinedContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/tabs",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Core logic for the tabs widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -26,10 +26,11 @@
26
26
  "url": "https://github.com/chakra-ui/zag/issues"
27
27
  },
28
28
  "dependencies": {
29
- "@zag-js/anatomy": "0.1.4",
29
+ "@zag-js/anatomy": "0.9.0",
30
30
  "@zag-js/dom-query": "0.1.4",
31
- "@zag-js/dom-event": "0.6.0",
32
- "@zag-js/tabbable": "0.1.1",
31
+ "@zag-js/dom-event": "0.8.0",
32
+ "@zag-js/element-rect": "0.8.0",
33
+ "@zag-js/tabbable": "0.8.0",
33
34
  "@zag-js/utils": "0.3.4",
34
35
  "@zag-js/core": "0.7.0",
35
36
  "@zag-js/types": "0.5.0"