@zag-js/tabs 0.6.0 → 0.8.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.
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-5ZMNIWHF.mjs";
3
+ } from "./chunk-SVFEUEGP.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;
@@ -21,7 +22,7 @@ function machine(userContext) {
21
22
  focusedValue: null,
22
23
  previousValues: [],
23
24
  indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
24
- hasMeasuredRect: false,
25
+ canIndicatorTransition: false,
25
26
  isIndicatorRendered: false,
26
27
  loop: true,
27
28
  translations: {},
@@ -32,10 +33,18 @@ function machine(userContext) {
32
33
  isVertical: (ctx2) => ctx2.orientation === "vertical"
33
34
  },
34
35
  created: ["setPrevSelectedTabs"],
36
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
37
+ exit: ["cleanupObserver"],
35
38
  watch: {
36
39
  focusedValue: "invokeOnFocus",
37
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
38
- dir: ["clearMeasured", "setIndicatorRect"]
40
+ value: [
41
+ "enableIndicatorTransition",
42
+ "invokeOnChange",
43
+ "setPrevSelectedTabs",
44
+ "syncIndicatorRect",
45
+ "setContentTabIndex"
46
+ ],
47
+ dir: ["syncIndicatorRect"]
39
48
  },
40
49
  on: {
41
50
  SET_VALUE: {
@@ -43,9 +52,11 @@ function machine(userContext) {
43
52
  },
44
53
  CLEAR_VALUE: {
45
54
  actions: "clearValue"
55
+ },
56
+ SET_INDICATOR_RECT: {
57
+ actions: "setIndicatorRect"
46
58
  }
47
59
  },
48
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
49
60
  states: {
50
61
  idle: {
51
62
  on: {
@@ -126,9 +137,6 @@ function machine(userContext) {
126
137
  clearValue(ctx2) {
127
138
  ctx2.value = null;
128
139
  },
129
- invokeOnDelete(ctx2, evt) {
130
- ctx2.onDelete?.({ value: evt.value });
131
- },
132
140
  focusFirstTab(ctx2) {
133
141
  raf(() => dom.getFirstEl(ctx2)?.focus());
134
142
  },
@@ -147,24 +155,9 @@ function machine(userContext) {
147
155
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
148
156
  raf(() => prev?.focus());
149
157
  },
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
158
  checkRenderedElements(ctx2) {
163
159
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
164
160
  },
165
- clearMeasured(ctx2) {
166
- ctx2.hasMeasuredRect = false;
167
- },
168
161
  invokeOnChange(ctx2) {
169
162
  ctx2.onChange?.({ value: ctx2.value });
170
163
  },
@@ -173,7 +166,7 @@ function machine(userContext) {
173
166
  },
174
167
  setPrevSelectedTabs(ctx2) {
175
168
  if (ctx2.value != null) {
176
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
169
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
177
170
  }
178
171
  },
179
172
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -189,13 +182,51 @@ function machine(userContext) {
189
182
  panel.setAttribute("tabindex", "0");
190
183
  }
191
184
  });
185
+ },
186
+ cleanupObserver(ctx2) {
187
+ ctx2.indicatorCleanup?.();
188
+ },
189
+ enableIndicatorTransition(ctx2) {
190
+ ctx2.canIndicatorTransition = true;
191
+ },
192
+ setIndicatorRect(ctx2, evt) {
193
+ const value = evt.id ?? ctx2.value;
194
+ if (!ctx2.isIndicatorRendered || !value)
195
+ return;
196
+ const tabEl = dom.getTriggerEl(ctx2, value);
197
+ if (!tabEl)
198
+ return;
199
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
200
+ nextTick(() => {
201
+ ctx2.canIndicatorTransition = false;
202
+ });
203
+ },
204
+ syncIndicatorRect(ctx2) {
205
+ ctx2.indicatorCleanup?.();
206
+ const value = ctx2.value;
207
+ if (!ctx2.isIndicatorRendered || !value)
208
+ return;
209
+ const tabEl = dom.getActiveTabEl(ctx2);
210
+ if (!tabEl)
211
+ return;
212
+ ctx2.indicatorCleanup = trackElementRect(tabEl, {
213
+ getRect(el) {
214
+ return dom.getOffsetRect(el);
215
+ },
216
+ onChange(rect) {
217
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
218
+ nextTick(() => {
219
+ ctx2.canIndicatorTransition = false;
220
+ });
221
+ }
222
+ });
192
223
  }
193
224
  }
194
225
  }
195
226
  );
196
227
  }
197
228
  function pushUnique(arr, value) {
198
- const newArr = arr.slice();
229
+ const newArr = Array.from(arr).slice();
199
230
  const index = newArr.indexOf(value);
200
231
  if (index > -1) {
201
232
  newArr.splice(index, 1);
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-S2KW2DT4.mjs";
4
4
  import {
5
5
  dom
6
- } from "./chunk-5ZMNIWHF.mjs";
6
+ } from "./chunk-SVFEUEGP.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
  }
@@ -7,7 +7,7 @@ var dom = createScope({
7
7
  getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
8
8
  getContentGroupId: (ctx) => ctx.ids?.contentGroup ?? `tabs:${ctx.id}:content-group`,
9
9
  getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
10
- getIndicatorId: (ctx) => `tabs:${ctx.id}:indicator`,
10
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
11
11
  getTablistEl: (ctx) => dom.getById(ctx, dom.getTablistId(ctx)),
12
12
  getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
13
13
  getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
@@ -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 prop = orientation === "vertical" ? "height" : "width";
44
48
  return {
45
- left: `${tab.offsetLeft}px`,
46
- width: `${tab.offsetWidth}px`
49
+ left: `${rect.left}px`,
50
+ top: `${rect.top}px`,
51
+ [prop]: `${rect[prop]}px`
47
52
  };
48
53
  }
49
54
  });
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var dom = (0, import_dom_query.createScope)({
44
44
  getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
45
45
  getContentGroupId: (ctx) => ctx.ids?.contentGroup ?? `tabs:${ctx.id}:content-group`,
46
46
  getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
47
- getIndicatorId: (ctx) => `tabs:${ctx.id}:indicator`,
47
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
48
48
  getTablistEl: (ctx) => dom.getById(ctx, dom.getTablistId(ctx)),
49
49
  getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
50
50
  getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
@@ -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 prop = orientation === "vertical" ? "height" : "width";
81
85
  return {
82
- left: `${tab.offsetLeft}px`,
83
- width: `${tab.offsetWidth}px`
86
+ left: `${rect.left}px`,
87
+ top: `${rect.top}px`,
88
+ [prop]: `${rect[prop]}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;
@@ -252,7 +264,7 @@ function machine(userContext) {
252
264
  focusedValue: null,
253
265
  previousValues: [],
254
266
  indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
255
- hasMeasuredRect: false,
267
+ canIndicatorTransition: false,
256
268
  isIndicatorRendered: false,
257
269
  loop: true,
258
270
  translations: {},
@@ -263,10 +275,18 @@ function machine(userContext) {
263
275
  isVertical: (ctx2) => ctx2.orientation === "vertical"
264
276
  },
265
277
  created: ["setPrevSelectedTabs"],
278
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
279
+ exit: ["cleanupObserver"],
266
280
  watch: {
267
281
  focusedValue: "invokeOnFocus",
268
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
269
- dir: ["clearMeasured", "setIndicatorRect"]
282
+ value: [
283
+ "enableIndicatorTransition",
284
+ "invokeOnChange",
285
+ "setPrevSelectedTabs",
286
+ "syncIndicatorRect",
287
+ "setContentTabIndex"
288
+ ],
289
+ dir: ["syncIndicatorRect"]
270
290
  },
271
291
  on: {
272
292
  SET_VALUE: {
@@ -274,9 +294,11 @@ function machine(userContext) {
274
294
  },
275
295
  CLEAR_VALUE: {
276
296
  actions: "clearValue"
297
+ },
298
+ SET_INDICATOR_RECT: {
299
+ actions: "setIndicatorRect"
277
300
  }
278
301
  },
279
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
280
302
  states: {
281
303
  idle: {
282
304
  on: {
@@ -357,9 +379,6 @@ function machine(userContext) {
357
379
  clearValue(ctx2) {
358
380
  ctx2.value = null;
359
381
  },
360
- invokeOnDelete(ctx2, evt) {
361
- ctx2.onDelete?.({ value: evt.value });
362
- },
363
382
  focusFirstTab(ctx2) {
364
383
  (0, import_dom_query3.raf)(() => dom.getFirstEl(ctx2)?.focus());
365
384
  },
@@ -378,24 +397,9 @@ function machine(userContext) {
378
397
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
379
398
  (0, import_dom_query3.raf)(() => prev?.focus());
380
399
  },
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
400
  checkRenderedElements(ctx2) {
394
401
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
395
402
  },
396
- clearMeasured(ctx2) {
397
- ctx2.hasMeasuredRect = false;
398
- },
399
403
  invokeOnChange(ctx2) {
400
404
  ctx2.onChange?.({ value: ctx2.value });
401
405
  },
@@ -404,7 +408,7 @@ function machine(userContext) {
404
408
  },
405
409
  setPrevSelectedTabs(ctx2) {
406
410
  if (ctx2.value != null) {
407
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
411
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
408
412
  }
409
413
  },
410
414
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -420,13 +424,51 @@ function machine(userContext) {
420
424
  panel.setAttribute("tabindex", "0");
421
425
  }
422
426
  });
427
+ },
428
+ cleanupObserver(ctx2) {
429
+ ctx2.indicatorCleanup?.();
430
+ },
431
+ enableIndicatorTransition(ctx2) {
432
+ ctx2.canIndicatorTransition = true;
433
+ },
434
+ setIndicatorRect(ctx2, evt) {
435
+ const value = evt.id ?? ctx2.value;
436
+ if (!ctx2.isIndicatorRendered || !value)
437
+ return;
438
+ const tabEl = dom.getTriggerEl(ctx2, value);
439
+ if (!tabEl)
440
+ return;
441
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
442
+ (0, import_dom_query3.nextTick)(() => {
443
+ ctx2.canIndicatorTransition = false;
444
+ });
445
+ },
446
+ syncIndicatorRect(ctx2) {
447
+ ctx2.indicatorCleanup?.();
448
+ const value = ctx2.value;
449
+ if (!ctx2.isIndicatorRendered || !value)
450
+ return;
451
+ const tabEl = dom.getActiveTabEl(ctx2);
452
+ if (!tabEl)
453
+ return;
454
+ ctx2.indicatorCleanup = (0, import_element_rect.trackElementRect)(tabEl, {
455
+ getRect(el) {
456
+ return dom.getOffsetRect(el);
457
+ },
458
+ onChange(rect) {
459
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
460
+ (0, import_dom_query3.nextTick)(() => {
461
+ ctx2.canIndicatorTransition = false;
462
+ });
463
+ }
464
+ });
423
465
  }
424
466
  }
425
467
  }
426
468
  );
427
469
  }
428
470
  function pushUnique(arr, value) {
429
- const newArr = arr.slice();
471
+ const newArr = Array.from(arr).slice();
430
472
  const index = newArr.indexOf(value);
431
473
  if (index > -1) {
432
474
  newArr.splice(index, 1);
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-5JW3YJ3I.mjs";
3
+ } from "./chunk-N7U3IQEQ.mjs";
4
4
  import {
5
5
  anatomy
6
6
  } from "./chunk-S2KW2DT4.mjs";
7
7
  import {
8
8
  machine
9
- } from "./chunk-JG2VX4ST.mjs";
10
- import "./chunk-5ZMNIWHF.mjs";
9
+ } from "./chunk-N5FVPJXW.mjs";
10
+ import "./chunk-SVFEUEGP.mjs";
11
11
  export {
12
12
  anatomy,
13
13
  connect,
@@ -23,6 +23,10 @@ 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
32
  getTriggerProps(props: TabProps): T["button"];
@@ -40,7 +40,7 @@ var dom = (0, import_dom_query.createScope)({
40
40
  getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
41
41
  getContentGroupId: (ctx) => ctx.ids?.contentGroup ?? `tabs:${ctx.id}:content-group`,
42
42
  getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
43
- getIndicatorId: (ctx) => `tabs:${ctx.id}:indicator`,
43
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
44
44
  getTablistEl: (ctx) => dom.getById(ctx, dom.getTablistId(ctx)),
45
45
  getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
46
46
  getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
@@ -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 prop = orientation === "vertical" ? "height" : "width";
77
81
  return {
78
- left: `${tab.offsetLeft}px`,
79
- width: `${tab.offsetWidth}px`
82
+ left: `${rect.left}px`,
83
+ top: `${rect.top}px`,
84
+ [prop]: `${rect[prop]}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-5JW3YJ3I.mjs";
3
+ } from "./chunk-N7U3IQEQ.mjs";
4
4
  import "./chunk-S2KW2DT4.mjs";
5
- import "./chunk-5ZMNIWHF.mjs";
5
+ import "./chunk-SVFEUEGP.mjs";
6
6
  export {
7
7
  connect
8
8
  };
@@ -38,16 +38,22 @@ 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) => {
49
+ [x: string]: string;
50
+ left: string;
42
51
  top: string;
43
- height: string;
44
- left?: undefined;
45
- width?: undefined;
46
- } | {
52
+ };
53
+ resolveRect(rect: Record<"width" | "height" | "left" | "top", number>, orientation?: "horizontal" | "vertical"): {
54
+ [x: string]: string;
47
55
  left: string;
48
- width: string;
49
- top?: undefined;
50
- height?: undefined;
56
+ top: string;
51
57
  };
52
58
  };
53
59
 
package/dist/tabs.dom.js CHANGED
@@ -31,7 +31,7 @@ var dom = (0, import_dom_query.createScope)({
31
31
  getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
32
32
  getContentGroupId: (ctx) => ctx.ids?.contentGroup ?? `tabs:${ctx.id}:content-group`,
33
33
  getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
34
- getIndicatorId: (ctx) => `tabs:${ctx.id}:indicator`,
34
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
35
35
  getTablistEl: (ctx) => dom.getById(ctx, dom.getTablistId(ctx)),
36
36
  getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
37
37
  getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
@@ -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 prop = orientation === "vertical" ? "height" : "width";
68
72
  return {
69
- left: `${tab.offsetLeft}px`,
70
- width: `${tab.offsetWidth}px`
73
+ left: `${rect.left}px`,
74
+ top: `${rect.top}px`,
75
+ [prop]: `${rect[prop]}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-5ZMNIWHF.mjs";
3
+ } from "./chunk-SVFEUEGP.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
 
@@ -37,7 +38,7 @@ var dom = (0, import_dom_query.createScope)({
37
38
  getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
38
39
  getContentGroupId: (ctx) => ctx.ids?.contentGroup ?? `tabs:${ctx.id}:content-group`,
39
40
  getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
40
- getIndicatorId: (ctx) => `tabs:${ctx.id}:indicator`,
41
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
41
42
  getTablistEl: (ctx) => dom.getById(ctx, dom.getTablistId(ctx)),
42
43
  getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
43
44
  getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
@@ -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 prop = orientation === "vertical" ? "height" : "width";
74
79
  return {
75
- left: `${tab.offsetLeft}px`,
76
- width: `${tab.offsetWidth}px`
80
+ left: `${rect.left}px`,
81
+ top: `${rect.top}px`,
82
+ [prop]: `${rect[prop]}px`
77
83
  };
78
84
  }
79
85
  });
@@ -93,7 +99,7 @@ function machine(userContext) {
93
99
  focusedValue: null,
94
100
  previousValues: [],
95
101
  indicatorRect: { left: "0px", top: "0px", width: "0px", height: "0px" },
96
- hasMeasuredRect: false,
102
+ canIndicatorTransition: false,
97
103
  isIndicatorRendered: false,
98
104
  loop: true,
99
105
  translations: {},
@@ -104,10 +110,18 @@ function machine(userContext) {
104
110
  isVertical: (ctx2) => ctx2.orientation === "vertical"
105
111
  },
106
112
  created: ["setPrevSelectedTabs"],
113
+ entry: ["checkRenderedElements", "syncIndicatorRect", "setContentTabIndex"],
114
+ exit: ["cleanupObserver"],
107
115
  watch: {
108
116
  focusedValue: "invokeOnFocus",
109
- value: ["invokeOnChange", "setPrevSelectedTabs", "setIndicatorRect", "setContentTabIndex"],
110
- dir: ["clearMeasured", "setIndicatorRect"]
117
+ value: [
118
+ "enableIndicatorTransition",
119
+ "invokeOnChange",
120
+ "setPrevSelectedTabs",
121
+ "syncIndicatorRect",
122
+ "setContentTabIndex"
123
+ ],
124
+ dir: ["syncIndicatorRect"]
111
125
  },
112
126
  on: {
113
127
  SET_VALUE: {
@@ -115,9 +129,11 @@ function machine(userContext) {
115
129
  },
116
130
  CLEAR_VALUE: {
117
131
  actions: "clearValue"
132
+ },
133
+ SET_INDICATOR_RECT: {
134
+ actions: "setIndicatorRect"
118
135
  }
119
136
  },
120
- entry: ["checkRenderedElements", "setIndicatorRect", "setContentTabIndex"],
121
137
  states: {
122
138
  idle: {
123
139
  on: {
@@ -198,9 +214,6 @@ function machine(userContext) {
198
214
  clearValue(ctx2) {
199
215
  ctx2.value = null;
200
216
  },
201
- invokeOnDelete(ctx2, evt) {
202
- ctx2.onDelete?.({ value: evt.value });
203
- },
204
217
  focusFirstTab(ctx2) {
205
218
  (0, import_dom_query2.raf)(() => dom.getFirstEl(ctx2)?.focus());
206
219
  },
@@ -219,24 +232,9 @@ function machine(userContext) {
219
232
  const prev = dom.getPrevEl(ctx2, ctx2.focusedValue);
220
233
  (0, import_dom_query2.raf)(() => prev?.focus());
221
234
  },
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
235
  checkRenderedElements(ctx2) {
235
236
  ctx2.isIndicatorRendered = !!dom.getIndicatorEl(ctx2);
236
237
  },
237
- clearMeasured(ctx2) {
238
- ctx2.hasMeasuredRect = false;
239
- },
240
238
  invokeOnChange(ctx2) {
241
239
  ctx2.onChange?.({ value: ctx2.value });
242
240
  },
@@ -245,7 +243,7 @@ function machine(userContext) {
245
243
  },
246
244
  setPrevSelectedTabs(ctx2) {
247
245
  if (ctx2.value != null) {
248
- ctx2.previousValues = pushUnique(Array.from(ctx2.previousValues), ctx2.value);
246
+ ctx2.previousValues = pushUnique(ctx2.previousValues, ctx2.value);
249
247
  }
250
248
  },
251
249
  // if tab panel contains focusable elements, remove the tabindex attribute
@@ -261,13 +259,51 @@ function machine(userContext) {
261
259
  panel.setAttribute("tabindex", "0");
262
260
  }
263
261
  });
262
+ },
263
+ cleanupObserver(ctx2) {
264
+ ctx2.indicatorCleanup?.();
265
+ },
266
+ enableIndicatorTransition(ctx2) {
267
+ ctx2.canIndicatorTransition = true;
268
+ },
269
+ setIndicatorRect(ctx2, evt) {
270
+ const value = evt.id ?? ctx2.value;
271
+ if (!ctx2.isIndicatorRendered || !value)
272
+ return;
273
+ const tabEl = dom.getTriggerEl(ctx2, value);
274
+ if (!tabEl)
275
+ return;
276
+ ctx2.indicatorRect = dom.getRectById(ctx2, value);
277
+ (0, import_dom_query2.nextTick)(() => {
278
+ ctx2.canIndicatorTransition = false;
279
+ });
280
+ },
281
+ syncIndicatorRect(ctx2) {
282
+ ctx2.indicatorCleanup?.();
283
+ const value = ctx2.value;
284
+ if (!ctx2.isIndicatorRendered || !value)
285
+ return;
286
+ const tabEl = dom.getActiveTabEl(ctx2);
287
+ if (!tabEl)
288
+ return;
289
+ ctx2.indicatorCleanup = (0, import_element_rect.trackElementRect)(tabEl, {
290
+ getRect(el) {
291
+ return dom.getOffsetRect(el);
292
+ },
293
+ onChange(rect) {
294
+ ctx2.indicatorRect = dom.resolveRect(rect, ctx2.orientation);
295
+ (0, import_dom_query2.nextTick)(() => {
296
+ ctx2.canIndicatorTransition = false;
297
+ });
298
+ }
299
+ });
264
300
  }
265
301
  }
266
302
  }
267
303
  );
268
304
  }
269
305
  function pushUnique(arr, value) {
270
- const newArr = arr.slice();
306
+ const newArr = Array.from(arr).slice();
271
307
  const index = newArr.indexOf(value);
272
308
  if (index > -1) {
273
309
  newArr.splice(index, 1);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  machine
3
- } from "./chunk-JG2VX4ST.mjs";
4
- import "./chunk-5ZMNIWHF.mjs";
3
+ } from "./chunk-N5FVPJXW.mjs";
4
+ import "./chunk-SVFEUEGP.mjs";
5
5
  export {
6
6
  machine
7
7
  };
@@ -10,6 +10,7 @@ type ElementIds = Partial<{
10
10
  tablist: string;
11
11
  contentGroup: string;
12
12
  content: string;
13
+ indicator: string;
13
14
  }>;
14
15
  type PublicContext = DirectionProperty & CommonProperties & {
15
16
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/tabs",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Core logic for the tabs widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -28,10 +28,11 @@
28
28
  "dependencies": {
29
29
  "@zag-js/anatomy": "0.1.4",
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
- "@zag-js/core": "0.5.0",
35
+ "@zag-js/core": "0.7.0",
35
36
  "@zag-js/types": "0.5.0"
36
37
  },
37
38
  "devDependencies": {