@destyler/tabs 0.0.5 → 0.0.7-beta.1

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.mjs CHANGED
@@ -1,8 +1,544 @@
1
- export { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger, tabsContentProps, tabsIndicatorProps, tabsListProps, tabsRootProps, tabsTriggerProps } from './component.mjs';
2
- import 'vue';
3
- import '@destyler/composition';
4
- import '@destyler/presence';
5
- import '@destyler/primitive';
6
- import '@destyler/directives';
7
- import '@destyler/shared';
8
- import '@destyler/roving-focus';
1
+ import { createAnatomy } from '@zag-js/anatomy';
2
+ import { getEventKey, clickIfLink } from '@zag-js/dom-event';
3
+ import { createScope, queryAll, nextById, prevById, itemById, dataAttr, isSelfTarget, isComposingEvent, isSafari, raf, getFocusables, nextTick } from '@zag-js/dom-query';
4
+ import { first, last, compact, isEqual, createSplitProps } from '@zag-js/utils';
5
+ import { createMachine, guards } from '@zag-js/core';
6
+ import { trackElementRect } from '@zag-js/element-rect';
7
+ import { createProps } from '@zag-js/types';
8
+
9
+ const anatomy = createAnatomy("tabs").parts("root", "list", "trigger", "content", "indicator");
10
+ const parts = anatomy.build();
11
+
12
+ const dom = createScope({
13
+ getRootId: (ctx) => ctx.ids?.root ?? `tabs:${ctx.id}`,
14
+ getListId: (ctx) => ctx.ids?.list ?? `tabs:${ctx.id}:list`,
15
+ getContentId: (ctx, id) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
16
+ getTriggerId: (ctx, id) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
17
+ getIndicatorId: (ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
18
+ getListEl: (ctx) => dom.getById(ctx, dom.getListId(ctx)),
19
+ getContentEl: (ctx, id) => dom.getById(ctx, dom.getContentId(ctx, id)),
20
+ getTriggerEl: (ctx, id) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
21
+ getIndicatorEl: (ctx) => dom.getById(ctx, dom.getIndicatorId(ctx)),
22
+ getElements: (ctx) => {
23
+ const ownerId = CSS.escape(dom.getListId(ctx));
24
+ const selector = `[role=tab][data-ownedby='${ownerId}']:not([disabled])`;
25
+ return queryAll(dom.getListEl(ctx), selector);
26
+ },
27
+ getFirstTriggerEl: (ctx) => first(dom.getElements(ctx)),
28
+ getLastTriggerEl: (ctx) => last(dom.getElements(ctx)),
29
+ getNextTriggerEl: (ctx, id) => nextById(dom.getElements(ctx), dom.getTriggerId(ctx, id), ctx.loopFocus),
30
+ getPrevTriggerEl: (ctx, id) => prevById(dom.getElements(ctx), dom.getTriggerId(ctx, id), ctx.loopFocus),
31
+ getSelectedContentEl: (ctx) => {
32
+ if (!ctx.value)
33
+ return;
34
+ return dom.getContentEl(ctx, ctx.value);
35
+ },
36
+ getSelectedTriggerEl: (ctx) => {
37
+ if (!ctx.value)
38
+ return;
39
+ return dom.getTriggerEl(ctx, ctx.value);
40
+ },
41
+ getOffsetRect: (el) => {
42
+ return {
43
+ left: el?.offsetLeft ?? 0,
44
+ top: el?.offsetTop ?? 0,
45
+ width: el?.offsetWidth ?? 0,
46
+ height: el?.offsetHeight ?? 0
47
+ };
48
+ },
49
+ getRectById: (ctx, id) => {
50
+ const tab = itemById(dom.getElements(ctx), dom.getTriggerId(ctx, id));
51
+ return dom.resolveRect(dom.getOffsetRect(tab));
52
+ },
53
+ resolveRect: (rect) => ({
54
+ width: `${rect.width}px`,
55
+ height: `${rect.height}px`,
56
+ left: `${rect.left}px`,
57
+ top: `${rect.top}px`
58
+ })
59
+ });
60
+
61
+ function connect(state, send, normalize) {
62
+ const translations = state.context.translations;
63
+ const focused = state.matches("focused");
64
+ const isVertical = state.context.orientation === "vertical";
65
+ const isHorizontal = state.context.orientation === "horizontal";
66
+ const composite = state.context.composite;
67
+ const indicator = state.context.indicatorState;
68
+ function getTriggerState(props) {
69
+ return {
70
+ selected: state.context.value === props.value,
71
+ focused: state.context.focusedValue === props.value,
72
+ disabled: !!props.disabled
73
+ };
74
+ }
75
+ return {
76
+ value: state.context.value,
77
+ focusedValue: state.context.focusedValue,
78
+ setValue(value) {
79
+ send({ type: "SET_VALUE", value });
80
+ },
81
+ clearValue() {
82
+ send({ type: "CLEAR_VALUE" });
83
+ },
84
+ setIndicatorRect(value) {
85
+ const id = dom.getTriggerId(state.context, value);
86
+ send({ type: "SET_INDICATOR_RECT", id });
87
+ },
88
+ syncTabIndex() {
89
+ send("SYNC_TAB_INDEX");
90
+ },
91
+ selectNext(fromValue) {
92
+ send({ type: "TAB_FOCUS", value: fromValue, src: "selectNext" });
93
+ send({ type: "ARROW_NEXT", src: "selectNext" });
94
+ },
95
+ selectPrev(fromValue) {
96
+ send({ type: "TAB_FOCUS", value: fromValue, src: "selectPrev" });
97
+ send({ type: "ARROW_PREV", src: "selectPrev" });
98
+ },
99
+ focus() {
100
+ dom.getSelectedTriggerEl(state.context)?.focus();
101
+ },
102
+ getRootProps() {
103
+ return normalize.element({
104
+ ...parts.root.attrs,
105
+ "id": dom.getRootId(state.context),
106
+ "data-orientation": state.context.orientation,
107
+ "data-focus": dataAttr(focused),
108
+ "dir": state.context.dir
109
+ });
110
+ },
111
+ getListProps() {
112
+ return normalize.element({
113
+ ...parts.list.attrs,
114
+ "id": dom.getListId(state.context),
115
+ "role": "tablist",
116
+ "dir": state.context.dir,
117
+ "data-focus": dataAttr(focused),
118
+ "aria-orientation": state.context.orientation,
119
+ "data-orientation": state.context.orientation,
120
+ "aria-label": translations?.listLabel,
121
+ onKeyDown(event) {
122
+ if (event.defaultPrevented)
123
+ return;
124
+ if (!isSelfTarget(event))
125
+ return;
126
+ if (isComposingEvent(event))
127
+ return;
128
+ const keyMap = {
129
+ ArrowDown() {
130
+ if (isHorizontal)
131
+ return;
132
+ send({ type: "ARROW_NEXT", key: "ArrowDown" });
133
+ },
134
+ ArrowUp() {
135
+ if (isHorizontal)
136
+ return;
137
+ send({ type: "ARROW_PREV", key: "ArrowUp" });
138
+ },
139
+ ArrowLeft() {
140
+ if (isVertical)
141
+ return;
142
+ send({ type: "ARROW_PREV", key: "ArrowLeft" });
143
+ },
144
+ ArrowRight() {
145
+ if (isVertical)
146
+ return;
147
+ send({ type: "ARROW_NEXT", key: "ArrowRight" });
148
+ },
149
+ Home() {
150
+ send("HOME");
151
+ },
152
+ End() {
153
+ send("END");
154
+ },
155
+ Enter() {
156
+ send({ type: "ENTER" });
157
+ }
158
+ };
159
+ const key = getEventKey(event, state.context);
160
+ const exec = keyMap[key];
161
+ if (exec) {
162
+ event.preventDefault();
163
+ exec(event);
164
+ }
165
+ }
166
+ });
167
+ },
168
+ getTriggerState,
169
+ getTriggerProps(props) {
170
+ const { value, disabled } = props;
171
+ const triggerState = getTriggerState(props);
172
+ return normalize.button({
173
+ ...parts.trigger.attrs,
174
+ "role": "tab",
175
+ "type": "button",
176
+ disabled,
177
+ "dir": state.context.dir,
178
+ "data-orientation": state.context.orientation,
179
+ "data-disabled": dataAttr(disabled),
180
+ "aria-disabled": disabled,
181
+ "data-value": value,
182
+ "aria-selected": triggerState.selected,
183
+ "data-selected": dataAttr(triggerState.selected),
184
+ "data-focus": dataAttr(triggerState.focused),
185
+ "aria-controls": triggerState.selected ? dom.getContentId(state.context, value) : void 0,
186
+ "data-ownedby": dom.getListId(state.context),
187
+ "data-ssr": dataAttr(state.context.ssr),
188
+ "id": dom.getTriggerId(state.context, value),
189
+ "tabIndex": triggerState.selected && composite ? 0 : -1,
190
+ onFocus() {
191
+ send({ type: "TAB_FOCUS", value });
192
+ },
193
+ onBlur(event) {
194
+ const target = event.relatedTarget;
195
+ if (target?.getAttribute("role") !== "tab") {
196
+ send({ type: "TAB_BLUR" });
197
+ }
198
+ },
199
+ onClick(event) {
200
+ if (event.defaultPrevented)
201
+ return;
202
+ if (disabled)
203
+ return;
204
+ if (isSafari()) {
205
+ event.currentTarget.focus();
206
+ }
207
+ send({ type: "TAB_CLICK", value });
208
+ }
209
+ });
210
+ },
211
+ getContentProps(props) {
212
+ const { value } = props;
213
+ const selected = state.context.value === value;
214
+ return normalize.element({
215
+ ...parts.content.attrs,
216
+ "dir": state.context.dir,
217
+ "id": dom.getContentId(state.context, value),
218
+ "tabIndex": composite ? 0 : -1,
219
+ "aria-labelledby": dom.getTriggerId(state.context, value),
220
+ "role": "tabpanel",
221
+ "data-ownedby": dom.getListId(state.context),
222
+ "data-selected": dataAttr(selected),
223
+ "data-orientation": state.context.orientation,
224
+ "hidden": !selected
225
+ });
226
+ },
227
+ getIndicatorProps() {
228
+ return normalize.element({
229
+ "id": dom.getIndicatorId(state.context),
230
+ ...parts.indicator.attrs,
231
+ "dir": state.context.dir,
232
+ "data-orientation": state.context.orientation,
233
+ "style": {
234
+ "--transition-property": "left, right, top, bottom, width, height",
235
+ "--left": indicator.rect?.left,
236
+ "--top": indicator.rect?.top,
237
+ "--width": indicator.rect?.width,
238
+ "--height": indicator.rect?.height,
239
+ "position": "absolute",
240
+ "willChange": "var(--transition-property)",
241
+ "transitionProperty": "var(--transition-property)",
242
+ "transitionDuration": indicator.transition ? "var(--transition-duration, 150ms)" : "0ms",
243
+ "transitionTimingFunction": "var(--transition-timing-function)",
244
+ [isHorizontal ? "left" : "top"]: isHorizontal ? "var(--left)" : "var(--top)"
245
+ }
246
+ });
247
+ }
248
+ };
249
+ }
250
+
251
+ const { not } = guards;
252
+ const invoke = {
253
+ change: (ctx) => {
254
+ if (ctx.value == null)
255
+ return;
256
+ ctx.onValueChange?.({ value: ctx.value });
257
+ },
258
+ focusChange: (ctx) => {
259
+ if (ctx.focusedValue == null)
260
+ return;
261
+ ctx.onFocusChange?.({ focusedValue: ctx.focusedValue });
262
+ }
263
+ };
264
+ const set = {
265
+ value: (ctx, value) => {
266
+ if (isEqual(value, ctx.value))
267
+ return;
268
+ ctx.value = value;
269
+ invoke.change(ctx);
270
+ },
271
+ focusedValue: (ctx, value) => {
272
+ if (isEqual(value, ctx.focusedValue))
273
+ return;
274
+ ctx.focusedValue = value;
275
+ invoke.focusChange(ctx);
276
+ }
277
+ };
278
+ function machine(userContext) {
279
+ const ctx = compact(userContext);
280
+ return createMachine(
281
+ {
282
+ initial: "idle",
283
+ context: {
284
+ dir: "ltr",
285
+ orientation: "horizontal",
286
+ activationMode: "automatic",
287
+ value: null,
288
+ loopFocus: true,
289
+ composite: true,
290
+ ...ctx,
291
+ focusedValue: ctx.value ?? null,
292
+ ssr: true,
293
+ indicatorState: {
294
+ rendered: false,
295
+ transition: false,
296
+ rect: { left: "0px", top: "0px", width: "0px", height: "0px" }
297
+ }
298
+ },
299
+ watch: {
300
+ value: ["allowIndicatorTransition", "syncIndicatorRect", "syncTabIndex", "clickIfLink"],
301
+ dir: ["syncIndicatorRect"],
302
+ orientation: ["syncIndicatorRect"]
303
+ },
304
+ on: {
305
+ SET_VALUE: {
306
+ actions: "setValue"
307
+ },
308
+ CLEAR_VALUE: {
309
+ actions: "clearValue"
310
+ },
311
+ SET_INDICATOR_RECT: {
312
+ actions: "setIndicatorRect"
313
+ },
314
+ SYNC_TAB_INDEX: {
315
+ actions: "syncTabIndex"
316
+ }
317
+ },
318
+ created: ["syncFocusedValue"],
319
+ entry: ["checkRenderedElements", "syncIndicatorRect", "syncTabIndex", "syncSsr"],
320
+ exit: ["cleanupObserver"],
321
+ states: {
322
+ idle: {
323
+ on: {
324
+ TAB_FOCUS: {
325
+ target: "focused",
326
+ actions: "setFocusedValue"
327
+ },
328
+ TAB_CLICK: {
329
+ target: "focused",
330
+ actions: ["setFocusedValue", "setValue"]
331
+ }
332
+ }
333
+ },
334
+ focused: {
335
+ on: {
336
+ TAB_CLICK: {
337
+ target: "focused",
338
+ actions: ["setFocusedValue", "setValue"]
339
+ },
340
+ ARROW_PREV: [
341
+ {
342
+ guard: "selectOnFocus",
343
+ actions: ["focusPrevTab", "selectFocusedTab"]
344
+ },
345
+ {
346
+ actions: "focusPrevTab"
347
+ }
348
+ ],
349
+ ARROW_NEXT: [
350
+ {
351
+ guard: "selectOnFocus",
352
+ actions: ["focusNextTab", "selectFocusedTab"]
353
+ },
354
+ {
355
+ actions: "focusNextTab"
356
+ }
357
+ ],
358
+ HOME: [
359
+ {
360
+ guard: "selectOnFocus",
361
+ actions: ["focusFirstTab", "selectFocusedTab"]
362
+ },
363
+ {
364
+ actions: "focusFirstTab"
365
+ }
366
+ ],
367
+ END: [
368
+ {
369
+ guard: "selectOnFocus",
370
+ actions: ["focusLastTab", "selectFocusedTab"]
371
+ },
372
+ {
373
+ actions: "focusLastTab"
374
+ }
375
+ ],
376
+ ENTER: {
377
+ guard: not("selectOnFocus"),
378
+ actions: "selectFocusedTab"
379
+ },
380
+ TAB_FOCUS: {
381
+ actions: ["setFocusedValue"]
382
+ },
383
+ TAB_BLUR: {
384
+ target: "idle",
385
+ actions: "clearFocusedValue"
386
+ }
387
+ }
388
+ }
389
+ }
390
+ },
391
+ {
392
+ guards: {
393
+ selectOnFocus: (ctx2) => ctx2.activationMode === "automatic"
394
+ },
395
+ actions: {
396
+ syncFocusedValue(ctx2) {
397
+ if (ctx2.value != null && ctx2.focusedValue == null) {
398
+ ctx2.focusedValue = ctx2.value;
399
+ }
400
+ },
401
+ selectFocusedTab(ctx2) {
402
+ raf(() => {
403
+ const nullable = ctx2.deselectable && ctx2.value === ctx2.focusedValue;
404
+ const value = nullable ? null : ctx2.focusedValue;
405
+ set.value(ctx2, value);
406
+ });
407
+ },
408
+ setFocusedValue(ctx2, evt) {
409
+ if (evt.value == null)
410
+ return;
411
+ set.focusedValue(ctx2, evt.value);
412
+ },
413
+ clearFocusedValue(ctx2) {
414
+ set.focusedValue(ctx2, null);
415
+ },
416
+ setValue(ctx2, evt) {
417
+ const nullable = ctx2.deselectable && ctx2.value === ctx2.focusedValue;
418
+ const value = nullable ? null : evt.value;
419
+ set.value(ctx2, value);
420
+ },
421
+ clearValue(ctx2) {
422
+ set.value(ctx2, null);
423
+ },
424
+ focusFirstTab(ctx2) {
425
+ raf(() => {
426
+ dom.getFirstTriggerEl(ctx2)?.focus();
427
+ });
428
+ },
429
+ focusLastTab(ctx2) {
430
+ raf(() => {
431
+ dom.getLastTriggerEl(ctx2)?.focus();
432
+ });
433
+ },
434
+ focusNextTab(ctx2) {
435
+ if (!ctx2.focusedValue)
436
+ return;
437
+ const triggerEl = dom.getNextTriggerEl(ctx2, ctx2.focusedValue);
438
+ raf(() => {
439
+ if (ctx2.composite) {
440
+ triggerEl?.focus();
441
+ } else if (triggerEl?.dataset.value != null) {
442
+ set.focusedValue(ctx2, triggerEl.dataset.value);
443
+ }
444
+ });
445
+ },
446
+ focusPrevTab(ctx2) {
447
+ if (!ctx2.focusedValue)
448
+ return;
449
+ const triggerEl = dom.getPrevTriggerEl(ctx2, ctx2.focusedValue);
450
+ raf(() => {
451
+ if (ctx2.composite) {
452
+ triggerEl?.focus();
453
+ } else if (triggerEl?.dataset.value != null) {
454
+ set.focusedValue(ctx2, triggerEl.dataset.value);
455
+ }
456
+ });
457
+ },
458
+ checkRenderedElements(ctx2) {
459
+ ctx2.indicatorState.rendered = !!dom.getIndicatorEl(ctx2);
460
+ },
461
+ syncTabIndex(ctx2) {
462
+ raf(() => {
463
+ const contentEl = dom.getSelectedContentEl(ctx2);
464
+ if (!contentEl)
465
+ return;
466
+ const focusables = getFocusables(contentEl);
467
+ if (focusables.length > 0) {
468
+ contentEl.removeAttribute("tabindex");
469
+ } else {
470
+ contentEl.setAttribute("tabindex", "0");
471
+ }
472
+ });
473
+ },
474
+ cleanupObserver(ctx2) {
475
+ ctx2.indicatorCleanup?.();
476
+ },
477
+ allowIndicatorTransition(ctx2) {
478
+ ctx2.indicatorState.transition = true;
479
+ },
480
+ setIndicatorRect(ctx2, evt) {
481
+ const value = evt.id ?? ctx2.value;
482
+ if (!ctx2.indicatorState.rendered || !value)
483
+ return;
484
+ const triggerEl = dom.getTriggerEl(ctx2, value);
485
+ if (!triggerEl)
486
+ return;
487
+ ctx2.indicatorState.rect = dom.getRectById(ctx2, value);
488
+ nextTick(() => {
489
+ ctx2.indicatorState.transition = false;
490
+ });
491
+ },
492
+ syncSsr(ctx2) {
493
+ ctx2.ssr = false;
494
+ },
495
+ syncIndicatorRect(ctx2) {
496
+ ctx2.indicatorCleanup?.();
497
+ const value = ctx2.value;
498
+ if (!ctx2.indicatorState.rendered || !value)
499
+ return;
500
+ const triggerEl = dom.getSelectedTriggerEl(ctx2);
501
+ if (!triggerEl)
502
+ return;
503
+ ctx2.indicatorCleanup = trackElementRect(triggerEl, {
504
+ getRect(el) {
505
+ return dom.getOffsetRect(el);
506
+ },
507
+ onChange(rect) {
508
+ ctx2.indicatorState.rect = dom.resolveRect(rect);
509
+ nextTick(() => {
510
+ ctx2.indicatorState.transition = false;
511
+ });
512
+ }
513
+ });
514
+ },
515
+ clickIfLink(ctx2) {
516
+ clickIfLink(dom.getSelectedTriggerEl(ctx2));
517
+ }
518
+ }
519
+ }
520
+ );
521
+ }
522
+
523
+ const props = createProps()([
524
+ "activationMode",
525
+ "composite",
526
+ "dir",
527
+ "getRootNode",
528
+ "id",
529
+ "ids",
530
+ "loopFocus",
531
+ "onFocusChange",
532
+ "onValueChange",
533
+ "orientation",
534
+ "translations",
535
+ "deselectable",
536
+ "value"
537
+ ]);
538
+ const splitProps = createSplitProps(props);
539
+ const triggerProps = createProps()(["disabled", "value"]);
540
+ const splitTriggerProps = createSplitProps(triggerProps);
541
+ const contentProps = createProps()(["value"]);
542
+ const splitContentProps = createSplitProps(contentProps);
543
+
544
+ export { anatomy, connect, contentProps, machine, props, splitContentProps, splitProps, splitTriggerProps, triggerProps };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@destyler/tabs",
3
3
  "type": "module",
4
- "version": "0.0.5",
5
- "description": "a set of layered sections of content—known as tab panels—that are displayed one at a time.",
4
+ "version": "0.0.7-beta.1",
5
+ "description": "A set of layered sections of content—known as tab panels—that are displayed one at a time.",
6
6
  "author": "Elone Hoo <elonehoo@gmail.com>",
7
7
  "license": "MIT",
8
8
  "exports": {
@@ -10,14 +10,6 @@
10
10
  "types": "./dist/index.d.mts",
11
11
  "default": "./dist/index.mjs"
12
12
  },
13
- "./resolver": {
14
- "types": "./dist/resolver.d.mts",
15
- "default": "./dist/resolver.mjs"
16
- },
17
- "./component": {
18
- "types": "./dist/component.d.mts",
19
- "default": "./dist/component.mjs"
20
- },
21
13
  "./*": "./*"
22
14
  },
23
15
  "main": "dist/index.cjs",
@@ -27,19 +19,18 @@
27
19
  "*.d.ts",
28
20
  "dist"
29
21
  ],
30
- "peerDependencies": {
31
- "vue": ">=3.4.21"
32
- },
33
22
  "dependencies": {
34
- "@destyler/directives": "0.0.5",
35
- "@destyler/primitive": "0.0.5",
36
- "@destyler/presence": "0.0.5",
37
- "@destyler/composition": "0.0.5",
38
- "@destyler/roving-focus": "0.0.5",
39
- "@destyler/shared": "0.0.5"
23
+ "@zag-js/anatomy": "^0.80.0",
24
+ "@zag-js/core": "^0.80.0",
25
+ "@zag-js/dom-event": "^0.80.0",
26
+ "@zag-js/dom-query": "^0.80.0",
27
+ "@zag-js/element-rect": "^0.80.0",
28
+ "@zag-js/types": "^0.80.0",
29
+ "@zag-js/utils": "^0.80.0"
40
30
  },
41
31
  "scripts": {
42
32
  "clear": "rimraf ./dist",
33
+ "clear:node_modules": "rimraf ./node_modules",
43
34
  "build": "pnpm run clear && unbuild",
44
35
  "stub": "unbuild --stub"
45
36
  }
package/README.md DELETED
@@ -1,7 +0,0 @@
1
- # @destyler/tabs
2
-
3
- [![NPM version](https://img.shields.io/npm/v/@destyler/tabs?color=a1b858&tabs=)](https://www.npmjs.com/package/@destyler/tabs)
4
-
5
- > a set of layered sections of content—known as tab panels—that are displayed one at a time.
6
-
7
- [GitHub](https://github.com/destyler/destyler) | [Documentation](https://destyler-dev.zeabur.app/)