@zag-js/tabs 0.70.0 → 0.72.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,201 +0,0 @@
1
- import { getEventKey, type EventKeyMap } from "@zag-js/dom-event"
2
- import { dataAttr, isComposingEvent, isSafari, isSelfTarget } from "@zag-js/dom-query"
3
- import type { NormalizeProps, PropTypes } from "@zag-js/types"
4
- import { parts } from "./tabs.anatomy"
5
- import { dom } from "./tabs.dom"
6
- import type { MachineApi, Send, State, TriggerProps, TriggerState } from "./tabs.types"
7
-
8
- export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
9
- const translations = state.context.translations
10
- const focused = state.matches("focused")
11
-
12
- const isVertical = state.context.orientation === "vertical"
13
- const isHorizontal = state.context.orientation === "horizontal"
14
- const composite = state.context.composite
15
- const indicator = state.context.indicatorState
16
-
17
- function getTriggerState(props: TriggerProps): TriggerState {
18
- return {
19
- selected: state.context.value === props.value,
20
- focused: state.context.focusedValue === props.value,
21
- disabled: !!props.disabled,
22
- }
23
- }
24
-
25
- return {
26
- value: state.context.value,
27
- focusedValue: state.context.focusedValue,
28
- setValue(value) {
29
- send({ type: "SET_VALUE", value })
30
- },
31
- clearValue() {
32
- send({ type: "CLEAR_VALUE" })
33
- },
34
- setIndicatorRect(value) {
35
- const id = dom.getTriggerId(state.context, value)
36
- send({ type: "SET_INDICATOR_RECT", id })
37
- },
38
- syncTabIndex() {
39
- send("SYNC_TAB_INDEX")
40
- },
41
- selectNext(fromValue) {
42
- send({ type: "TAB_FOCUS", value: fromValue, src: "selectNext" })
43
- send({ type: "ARROW_NEXT", src: "selectNext" })
44
- },
45
- selectPrev(fromValue) {
46
- send({ type: "TAB_FOCUS", value: fromValue, src: "selectPrev" })
47
- send({ type: "ARROW_PREV", src: "selectPrev" })
48
- },
49
- focus() {
50
- dom.getSelectedTriggerEl(state.context)?.focus()
51
- },
52
-
53
- getRootProps() {
54
- return normalize.element({
55
- ...parts.root.attrs,
56
- id: dom.getRootId(state.context),
57
- "data-orientation": state.context.orientation,
58
- "data-focus": dataAttr(focused),
59
- dir: state.context.dir,
60
- })
61
- },
62
-
63
- getListProps() {
64
- return normalize.element({
65
- ...parts.list.attrs,
66
- id: dom.getListId(state.context),
67
- role: "tablist",
68
- dir: state.context.dir,
69
- "data-focus": dataAttr(focused),
70
- "aria-orientation": state.context.orientation,
71
- "data-orientation": state.context.orientation,
72
- "aria-label": translations?.listLabel,
73
- onKeyDown(event) {
74
- if (event.defaultPrevented) return
75
-
76
- if (!isSelfTarget(event)) return
77
- if (isComposingEvent(event)) return
78
-
79
- const keyMap: EventKeyMap = {
80
- ArrowDown() {
81
- if (isHorizontal) return
82
- send({ type: "ARROW_NEXT", key: "ArrowDown" })
83
- },
84
- ArrowUp() {
85
- if (isHorizontal) return
86
- send({ type: "ARROW_PREV", key: "ArrowUp" })
87
- },
88
- ArrowLeft() {
89
- if (isVertical) return
90
- send({ type: "ARROW_PREV", key: "ArrowLeft" })
91
- },
92
- ArrowRight() {
93
- if (isVertical) return
94
- send({ type: "ARROW_NEXT", key: "ArrowRight" })
95
- },
96
- Home() {
97
- send("HOME")
98
- },
99
- End() {
100
- send("END")
101
- },
102
- Enter() {
103
- send({ type: "ENTER" })
104
- },
105
- }
106
-
107
- let key = getEventKey(event, state.context)
108
- const exec = keyMap[key]
109
-
110
- if (exec) {
111
- event.preventDefault()
112
- exec(event)
113
- }
114
- },
115
- })
116
- },
117
-
118
- getTriggerState,
119
-
120
- getTriggerProps(props) {
121
- const { value, disabled } = props
122
- const triggerState = getTriggerState(props)
123
-
124
- return normalize.button({
125
- ...parts.trigger.attrs,
126
- role: "tab",
127
- type: "button",
128
- disabled,
129
- dir: state.context.dir,
130
- "data-orientation": state.context.orientation,
131
- "data-disabled": dataAttr(disabled),
132
- "aria-disabled": disabled,
133
- "data-value": value,
134
- "aria-selected": triggerState.selected,
135
- "data-selected": dataAttr(triggerState.selected),
136
- "data-focus": dataAttr(triggerState.focused),
137
- "aria-controls": triggerState.selected ? dom.getContentId(state.context, value) : undefined,
138
- "data-ownedby": dom.getListId(state.context),
139
- "data-ssr": dataAttr(state.context.ssr),
140
- id: dom.getTriggerId(state.context, value),
141
- tabIndex: triggerState.selected && composite ? 0 : -1,
142
- onFocus() {
143
- send({ type: "TAB_FOCUS", value })
144
- },
145
- onBlur(event) {
146
- const target = event.relatedTarget as HTMLElement | null
147
- if (target?.getAttribute("role") !== "tab") {
148
- send({ type: "TAB_BLUR" })
149
- }
150
- },
151
- onClick(event) {
152
- if (event.defaultPrevented) return
153
- if (disabled) return
154
- if (isSafari()) {
155
- event.currentTarget.focus()
156
- }
157
- send({ type: "TAB_CLICK", value })
158
- },
159
- })
160
- },
161
-
162
- getContentProps(props) {
163
- const { value } = props
164
- const selected = state.context.value === value
165
- return normalize.element({
166
- ...parts.content.attrs,
167
- dir: state.context.dir,
168
- id: dom.getContentId(state.context, value),
169
- tabIndex: composite ? 0 : -1,
170
- "aria-labelledby": dom.getTriggerId(state.context, value),
171
- role: "tabpanel",
172
- "data-ownedby": dom.getListId(state.context),
173
- "data-selected": dataAttr(selected),
174
- "data-orientation": state.context.orientation,
175
- hidden: !selected,
176
- })
177
- },
178
-
179
- getIndicatorProps() {
180
- return normalize.element({
181
- id: dom.getIndicatorId(state.context),
182
- ...parts.indicator.attrs,
183
- dir: state.context.dir,
184
- "data-orientation": state.context.orientation,
185
- style: {
186
- "--transition-property": "left, right, top, bottom, width, height",
187
- "--left": indicator.rect?.left,
188
- "--top": indicator.rect?.top,
189
- "--width": indicator.rect?.width,
190
- "--height": indicator.rect?.height,
191
- position: "absolute",
192
- willChange: "var(--transition-property)",
193
- transitionProperty: "var(--transition-property)",
194
- transitionDuration: indicator.transition ? "var(--transition-duration, 150ms)" : "0ms",
195
- transitionTimingFunction: "var(--transition-timing-function)",
196
- [isHorizontal ? "left" : "top"]: isHorizontal ? "var(--left)" : "var(--top)",
197
- },
198
- })
199
- },
200
- }
201
- }
package/src/tabs.dom.ts DELETED
@@ -1,56 +0,0 @@
1
- import { createScope, itemById, nextById, prevById, queryAll } from "@zag-js/dom-query"
2
- import { first, last } from "@zag-js/utils"
3
- import type { MachineContext as Ctx } from "./tabs.types"
4
-
5
- export const dom = createScope({
6
- getRootId: (ctx: Ctx) => ctx.ids?.root ?? `tabs:${ctx.id}`,
7
- getListId: (ctx: Ctx) => ctx.ids?.list ?? `tabs:${ctx.id}:list`,
8
- getContentId: (ctx: Ctx, id: string) => ctx.ids?.content ?? `tabs:${ctx.id}:content-${id}`,
9
- getTriggerId: (ctx: Ctx, id: string) => ctx.ids?.trigger ?? `tabs:${ctx.id}:trigger-${id}`,
10
- getIndicatorId: (ctx: Ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
11
-
12
- getListEl: (ctx: Ctx) => dom.getById(ctx, dom.getListId(ctx)),
13
- getContentEl: (ctx: Ctx, id: string) => dom.getById(ctx, dom.getContentId(ctx, id)),
14
- getTriggerEl: (ctx: Ctx, id: string) => dom.getById(ctx, dom.getTriggerId(ctx, id)),
15
- getIndicatorEl: (ctx: Ctx) => dom.getById(ctx, dom.getIndicatorId(ctx)),
16
-
17
- getElements: (ctx: Ctx) => {
18
- const ownerId = CSS.escape(dom.getListId(ctx))
19
- const selector = `[role=tab][data-ownedby='${ownerId}']:not([disabled])`
20
- return queryAll(dom.getListEl(ctx), selector)
21
- },
22
-
23
- getFirstTriggerEl: (ctx: Ctx) => first(dom.getElements(ctx)),
24
- getLastTriggerEl: (ctx: Ctx) => last(dom.getElements(ctx)),
25
- getNextTriggerEl: (ctx: Ctx, id: string) => nextById(dom.getElements(ctx), dom.getTriggerId(ctx, id), ctx.loopFocus),
26
- getPrevTriggerEl: (ctx: Ctx, id: string) => prevById(dom.getElements(ctx), dom.getTriggerId(ctx, id), ctx.loopFocus),
27
- getSelectedContentEl: (ctx: Ctx) => {
28
- if (!ctx.value) return
29
- return dom.getContentEl(ctx, ctx.value)
30
- },
31
- getSelectedTriggerEl: (ctx: Ctx) => {
32
- if (!ctx.value) return
33
- return dom.getTriggerEl(ctx, ctx.value)
34
- },
35
-
36
- getOffsetRect: (el: HTMLElement | undefined) => {
37
- return {
38
- left: el?.offsetLeft ?? 0,
39
- top: el?.offsetTop ?? 0,
40
- width: el?.offsetWidth ?? 0,
41
- height: el?.offsetHeight ?? 0,
42
- }
43
- },
44
-
45
- getRectById: (ctx: Ctx, id: string) => {
46
- const tab = itemById(dom.getElements(ctx), dom.getTriggerId(ctx, id))
47
- return dom.resolveRect(dom.getOffsetRect(tab))
48
- },
49
-
50
- resolveRect: (rect: Record<"width" | "height" | "left" | "top", number>) => ({
51
- width: `${rect.width}px`,
52
- height: `${rect.height}px`,
53
- left: `${rect.left}px`,
54
- top: `${rect.top}px`,
55
- }),
56
- })
@@ -1,283 +0,0 @@
1
- import { createMachine, guards } from "@zag-js/core"
2
- import { clickIfLink } from "@zag-js/dom-event"
3
- import { nextTick, raf, getFocusables } from "@zag-js/dom-query"
4
- import { trackElementRect } from "@zag-js/element-rect"
5
- import { compact, isEqual } from "@zag-js/utils"
6
- import { dom } from "./tabs.dom"
7
- import type { MachineContext, MachineState, UserDefinedContext } from "./tabs.types"
8
-
9
- const { not } = guards
10
-
11
- export function machine(userContext: UserDefinedContext) {
12
- const ctx = compact(userContext)
13
- return createMachine<MachineContext, MachineState>(
14
- {
15
- initial: "idle",
16
-
17
- context: {
18
- dir: "ltr",
19
- orientation: "horizontal",
20
- activationMode: "automatic",
21
- value: null,
22
- loopFocus: true,
23
- composite: true,
24
- ...ctx,
25
- focusedValue: ctx.value ?? null,
26
- ssr: true,
27
- indicatorState: {
28
- rendered: false,
29
- transition: false,
30
- rect: { left: "0px", top: "0px", width: "0px", height: "0px" },
31
- },
32
- },
33
-
34
- watch: {
35
- value: ["allowIndicatorTransition", "syncIndicatorRect", "syncTabIndex", "clickIfLink"],
36
- dir: ["syncIndicatorRect"],
37
- orientation: ["syncIndicatorRect"],
38
- },
39
-
40
- on: {
41
- SET_VALUE: {
42
- actions: "setValue",
43
- },
44
- CLEAR_VALUE: {
45
- actions: "clearValue",
46
- },
47
- SET_INDICATOR_RECT: {
48
- actions: "setIndicatorRect",
49
- },
50
- SYNC_TAB_INDEX: {
51
- actions: "syncTabIndex",
52
- },
53
- },
54
-
55
- created: ["syncFocusedValue"],
56
-
57
- entry: ["checkRenderedElements", "syncIndicatorRect", "syncTabIndex", "syncSsr"],
58
-
59
- exit: ["cleanupObserver"],
60
-
61
- states: {
62
- idle: {
63
- on: {
64
- TAB_FOCUS: {
65
- target: "focused",
66
- actions: "setFocusedValue",
67
- },
68
- TAB_CLICK: {
69
- target: "focused",
70
- actions: ["setFocusedValue", "setValue"],
71
- },
72
- },
73
- },
74
- focused: {
75
- on: {
76
- TAB_CLICK: {
77
- target: "focused",
78
- actions: ["setFocusedValue", "setValue"],
79
- },
80
- ARROW_PREV: [
81
- {
82
- guard: "selectOnFocus",
83
- actions: ["focusPrevTab", "selectFocusedTab"],
84
- },
85
- {
86
- actions: "focusPrevTab",
87
- },
88
- ],
89
- ARROW_NEXT: [
90
- {
91
- guard: "selectOnFocus",
92
- actions: ["focusNextTab", "selectFocusedTab"],
93
- },
94
- {
95
- actions: "focusNextTab",
96
- },
97
- ],
98
- HOME: [
99
- {
100
- guard: "selectOnFocus",
101
- actions: ["focusFirstTab", "selectFocusedTab"],
102
- },
103
- {
104
- actions: "focusFirstTab",
105
- },
106
- ],
107
- END: [
108
- {
109
- guard: "selectOnFocus",
110
- actions: ["focusLastTab", "selectFocusedTab"],
111
- },
112
- {
113
- actions: "focusLastTab",
114
- },
115
- ],
116
- ENTER: {
117
- guard: not("selectOnFocus"),
118
- actions: "selectFocusedTab",
119
- },
120
- TAB_FOCUS: {
121
- actions: ["setFocusedValue"],
122
- },
123
- TAB_BLUR: {
124
- target: "idle",
125
- actions: "clearFocusedValue",
126
- },
127
- },
128
- },
129
- },
130
- },
131
- {
132
- guards: {
133
- selectOnFocus: (ctx) => ctx.activationMode === "automatic",
134
- },
135
-
136
- actions: {
137
- syncFocusedValue(ctx) {
138
- if (ctx.value != null && ctx.focusedValue == null) {
139
- ctx.focusedValue = ctx.value
140
- }
141
- },
142
- selectFocusedTab(ctx) {
143
- raf(() => {
144
- const nullable = ctx.deselectable && ctx.value === ctx.focusedValue
145
- const value = nullable ? null : ctx.focusedValue
146
- set.value(ctx, value)
147
- })
148
- },
149
- setFocusedValue(ctx, evt) {
150
- if (evt.value == null) return
151
- set.focusedValue(ctx, evt.value)
152
- },
153
- clearFocusedValue(ctx) {
154
- set.focusedValue(ctx, null)
155
- },
156
- setValue(ctx, evt) {
157
- const nullable = ctx.deselectable && ctx.value === ctx.focusedValue
158
- const value = nullable ? null : evt.value
159
- set.value(ctx, value)
160
- },
161
- clearValue(ctx) {
162
- set.value(ctx, null)
163
- },
164
- focusFirstTab(ctx) {
165
- raf(() => {
166
- dom.getFirstTriggerEl(ctx)?.focus()
167
- })
168
- },
169
- focusLastTab(ctx) {
170
- raf(() => {
171
- dom.getLastTriggerEl(ctx)?.focus()
172
- })
173
- },
174
- focusNextTab(ctx) {
175
- if (!ctx.focusedValue) return
176
- const triggerEl = dom.getNextTriggerEl(ctx, ctx.focusedValue)
177
- raf(() => {
178
- if (ctx.composite) {
179
- triggerEl?.focus()
180
- } else if (triggerEl?.dataset.value != null) {
181
- set.focusedValue(ctx, triggerEl.dataset.value)
182
- }
183
- })
184
- },
185
- focusPrevTab(ctx) {
186
- if (!ctx.focusedValue) return
187
- const triggerEl = dom.getPrevTriggerEl(ctx, ctx.focusedValue)
188
- raf(() => {
189
- if (ctx.composite) {
190
- triggerEl?.focus()
191
- } else if (triggerEl?.dataset.value != null) {
192
- set.focusedValue(ctx, triggerEl.dataset.value)
193
- }
194
- })
195
- },
196
- checkRenderedElements(ctx) {
197
- ctx.indicatorState.rendered = !!dom.getIndicatorEl(ctx)
198
- },
199
- syncTabIndex(ctx) {
200
- raf(() => {
201
- const contentEl = dom.getSelectedContentEl(ctx)
202
- if (!contentEl) return
203
- const focusables = getFocusables(contentEl)
204
- if (focusables.length > 0) {
205
- contentEl.removeAttribute("tabindex")
206
- } else {
207
- contentEl.setAttribute("tabindex", "0")
208
- }
209
- })
210
- },
211
- cleanupObserver(ctx) {
212
- ctx.indicatorCleanup?.()
213
- },
214
- allowIndicatorTransition(ctx) {
215
- ctx.indicatorState.transition = true
216
- },
217
- setIndicatorRect(ctx, evt) {
218
- const value = evt.id ?? ctx.value
219
- if (!ctx.indicatorState.rendered || !value) return
220
-
221
- const triggerEl = dom.getTriggerEl(ctx, value)
222
- if (!triggerEl) return
223
-
224
- ctx.indicatorState.rect = dom.getRectById(ctx, value)
225
- nextTick(() => {
226
- ctx.indicatorState.transition = false
227
- })
228
- },
229
- syncSsr(ctx) {
230
- ctx.ssr = false
231
- },
232
- syncIndicatorRect(ctx) {
233
- ctx.indicatorCleanup?.()
234
-
235
- const value = ctx.value
236
- if (!ctx.indicatorState.rendered || !value) return
237
-
238
- const triggerEl = dom.getSelectedTriggerEl(ctx)
239
- if (!triggerEl) return
240
-
241
- ctx.indicatorCleanup = trackElementRect(triggerEl, {
242
- getRect(el) {
243
- return dom.getOffsetRect(el)
244
- },
245
- onChange(rect) {
246
- ctx.indicatorState.rect = dom.resolveRect(rect)
247
- nextTick(() => {
248
- ctx.indicatorState.transition = false
249
- })
250
- },
251
- })
252
- },
253
- clickIfLink(ctx) {
254
- clickIfLink(dom.getSelectedTriggerEl(ctx))
255
- },
256
- },
257
- },
258
- )
259
- }
260
-
261
- const invoke = {
262
- change: (ctx: MachineContext) => {
263
- if (ctx.value == null) return
264
- ctx.onValueChange?.({ value: ctx.value })
265
- },
266
- focusChange: (ctx: MachineContext) => {
267
- if (ctx.focusedValue == null) return
268
- ctx.onFocusChange?.({ focusedValue: ctx.focusedValue })
269
- },
270
- }
271
-
272
- const set = {
273
- value: (ctx: MachineContext, value: string | null) => {
274
- if (isEqual(value, ctx.value)) return
275
- ctx.value = value
276
- invoke.change(ctx)
277
- },
278
- focusedValue: (ctx: MachineContext, value: string | null) => {
279
- if (isEqual(value, ctx.focusedValue)) return
280
- ctx.focusedValue = value
281
- invoke.focusChange(ctx)
282
- },
283
- }
package/src/tabs.props.ts DELETED
@@ -1,27 +0,0 @@
1
- import { createProps } from "@zag-js/types"
2
- import { createSplitProps } from "@zag-js/utils"
3
- import type { ContentProps, TriggerProps, UserDefinedContext } from "./tabs.types"
4
-
5
- export const props = createProps<UserDefinedContext>()([
6
- "activationMode",
7
- "composite",
8
- "dir",
9
- "getRootNode",
10
- "id",
11
- "ids",
12
- "loopFocus",
13
- "onFocusChange",
14
- "onValueChange",
15
- "orientation",
16
- "translations",
17
- "deselectable",
18
- "value",
19
- ])
20
-
21
- export const splitProps = createSplitProps<Partial<UserDefinedContext>>(props)
22
-
23
- export const triggerProps = createProps<TriggerProps>()(["disabled", "value"])
24
- export const splitTriggerProps = createSplitProps<TriggerProps>(triggerProps)
25
-
26
- export const contentProps = createProps<ContentProps>()(["value"])
27
- export const splitContentProps = createSplitProps<ContentProps>(contentProps)