@zag-js/radio-group 0.9.2 → 0.10.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/package.json +11 -10
- package/src/index.ts +4 -0
- package/src/radio-group.anatomy.ts +12 -0
- package/src/radio-group.connect.ts +224 -0
- package/src/radio-group.dom.ts +57 -0
- package/src/radio-group.machine.ts +138 -0
- package/src/radio-group.types.ts +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/radio-group",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Core logic for the radio group widget implemented as a state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/radio-group",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"files": [
|
|
21
|
-
"dist
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
22
23
|
],
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"access": "public"
|
|
@@ -27,14 +28,14 @@
|
|
|
27
28
|
"url": "https://github.com/chakra-ui/zag/issues"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@zag-js/anatomy": "0.
|
|
31
|
-
"@zag-js/dom-query": "0.
|
|
32
|
-
"@zag-js/element-rect": "0.
|
|
33
|
-
"@zag-js/form-utils": "0.
|
|
34
|
-
"@zag-js/visually-hidden": "0.
|
|
35
|
-
"@zag-js/utils": "0.
|
|
36
|
-
"@zag-js/core": "0.
|
|
37
|
-
"@zag-js/types": "0.
|
|
31
|
+
"@zag-js/anatomy": "0.10.1",
|
|
32
|
+
"@zag-js/dom-query": "0.10.1",
|
|
33
|
+
"@zag-js/element-rect": "0.10.1",
|
|
34
|
+
"@zag-js/form-utils": "0.10.1",
|
|
35
|
+
"@zag-js/visually-hidden": "0.10.1",
|
|
36
|
+
"@zag-js/utils": "0.10.1",
|
|
37
|
+
"@zag-js/core": "0.10.1",
|
|
38
|
+
"@zag-js/types": "0.10.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"clean-package": "2.2.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ariaAttr, dataAttr } from "@zag-js/dom-query"
|
|
2
|
+
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
3
|
+
import { visuallyHiddenStyle } from "@zag-js/visually-hidden"
|
|
4
|
+
import { parts } from "./radio-group.anatomy"
|
|
5
|
+
import { dom } from "./radio-group.dom"
|
|
6
|
+
import type { InputProps, RadioProps, Send, State } from "./radio-group.types"
|
|
7
|
+
|
|
8
|
+
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
|
|
9
|
+
const isGroupDisabled = state.context.disabled
|
|
10
|
+
const isGroupReadOnly = state.context.readOnly
|
|
11
|
+
|
|
12
|
+
function getRadioState<T extends RadioProps>(props: T) {
|
|
13
|
+
const radioState = {
|
|
14
|
+
isReadOnly: props.readOnly || isGroupReadOnly,
|
|
15
|
+
isInvalid: props.invalid,
|
|
16
|
+
isDisabled: props.disabled || isGroupDisabled,
|
|
17
|
+
isChecked: state.context.value === props.value,
|
|
18
|
+
isFocused: state.context.focusedId === props.value,
|
|
19
|
+
isHovered: state.context.hoveredId === props.value,
|
|
20
|
+
isActive: state.context.activeId === props.value,
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
...radioState,
|
|
24
|
+
isInteractive: !(radioState.isReadOnly || radioState.isDisabled),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getRadioDataAttrs<T extends RadioProps>(props: T) {
|
|
29
|
+
const radioState = getRadioState(props)
|
|
30
|
+
return {
|
|
31
|
+
"data-focus": dataAttr(radioState.isFocused),
|
|
32
|
+
"data-disabled": dataAttr(radioState.isDisabled),
|
|
33
|
+
"data-checked": dataAttr(radioState.isChecked),
|
|
34
|
+
"data-hover": dataAttr(radioState.isHovered),
|
|
35
|
+
"data-invalid": dataAttr(radioState.isInvalid),
|
|
36
|
+
"data-readonly": dataAttr(radioState.isReadOnly),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const focus = () => {
|
|
41
|
+
const firstEnabledAndCheckedInput = dom.getFirstEnabledAndCheckedInputEl(state.context)
|
|
42
|
+
|
|
43
|
+
if (firstEnabledAndCheckedInput) {
|
|
44
|
+
firstEnabledAndCheckedInput.focus()
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const firstEnabledInput = dom.getFirstEnabledInputEl(state.context)
|
|
49
|
+
firstEnabledInput?.focus()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* The current value of the radio group
|
|
55
|
+
*/
|
|
56
|
+
value: state.context.value,
|
|
57
|
+
/**
|
|
58
|
+
* Function to set the value of the radio group
|
|
59
|
+
*/
|
|
60
|
+
setValue(value: string) {
|
|
61
|
+
send({ type: "SET_VALUE", value, manual: true })
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Function to clear the value of the radio group
|
|
65
|
+
*/
|
|
66
|
+
clearValue() {
|
|
67
|
+
send({ type: "SET_VALUE", value: null, manual: true })
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Function to focus the radio group
|
|
71
|
+
*/
|
|
72
|
+
focus,
|
|
73
|
+
/**
|
|
74
|
+
* Function to blur the currently focused radio input in the radio group
|
|
75
|
+
*/
|
|
76
|
+
blur() {
|
|
77
|
+
const focusedElement = dom.getActiveElement(state.context)
|
|
78
|
+
const inputEls = dom.getInputEls(state.context)
|
|
79
|
+
const radioInputIsFocused = inputEls.some((el) => el === focusedElement)
|
|
80
|
+
if (radioInputIsFocused) focusedElement?.blur()
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Returns the state details of a radio input
|
|
84
|
+
*/
|
|
85
|
+
getRadioState,
|
|
86
|
+
|
|
87
|
+
rootProps: normalize.element({
|
|
88
|
+
...parts.root.attrs,
|
|
89
|
+
role: "radiogroup",
|
|
90
|
+
id: dom.getRootId(state.context),
|
|
91
|
+
"aria-labelledby": dom.getLabelId(state.context),
|
|
92
|
+
"data-orientation": state.context.orientation,
|
|
93
|
+
"aria-orientation": state.context.orientation,
|
|
94
|
+
dir: state.context.dir,
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
labelProps: normalize.element({
|
|
98
|
+
...parts.label.attrs,
|
|
99
|
+
id: dom.getLabelId(state.context),
|
|
100
|
+
onClick: focus,
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
getRadioProps(props: RadioProps) {
|
|
104
|
+
const rootState = getRadioState(props)
|
|
105
|
+
|
|
106
|
+
return normalize.label({
|
|
107
|
+
...parts.radio.attrs,
|
|
108
|
+
id: dom.getRadioId(state.context, props.value),
|
|
109
|
+
htmlFor: dom.getRadioInputId(state.context, props.value),
|
|
110
|
+
...getRadioDataAttrs(props),
|
|
111
|
+
|
|
112
|
+
onPointerMove() {
|
|
113
|
+
if (!rootState.isInteractive) return
|
|
114
|
+
send({ type: "SET_HOVERED", value: props.value, hovered: true })
|
|
115
|
+
},
|
|
116
|
+
onPointerLeave() {
|
|
117
|
+
if (!rootState.isInteractive) return
|
|
118
|
+
send({ type: "SET_HOVERED", value: null })
|
|
119
|
+
},
|
|
120
|
+
onPointerDown(event) {
|
|
121
|
+
if (!rootState.isInteractive) return
|
|
122
|
+
// On pointerdown, the input blurs and returns focus to the `body`,
|
|
123
|
+
// we need to prevent this.
|
|
124
|
+
if (rootState.isFocused && event.pointerType === "mouse") {
|
|
125
|
+
event.preventDefault()
|
|
126
|
+
}
|
|
127
|
+
send({ type: "SET_ACTIVE", value: props.value, active: true })
|
|
128
|
+
},
|
|
129
|
+
onPointerUp() {
|
|
130
|
+
if (!rootState.isInteractive) return
|
|
131
|
+
send({ type: "SET_ACTIVE", value: null })
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
getRadioLabelProps(props: RadioProps) {
|
|
137
|
+
return normalize.element({
|
|
138
|
+
...parts.radioLabel.attrs,
|
|
139
|
+
id: dom.getRadioLabelId(state.context, props.value),
|
|
140
|
+
...getRadioDataAttrs(props),
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
getRadioControlProps(props: RadioProps) {
|
|
145
|
+
const controlState = getRadioState(props)
|
|
146
|
+
|
|
147
|
+
return normalize.element({
|
|
148
|
+
...parts.radioControl.attrs,
|
|
149
|
+
id: dom.getRadioControlId(state.context, props.value),
|
|
150
|
+
"data-active": dataAttr(controlState.isActive),
|
|
151
|
+
"aria-hidden": true,
|
|
152
|
+
...getRadioDataAttrs(props),
|
|
153
|
+
})
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
getRadioInputProps(props: InputProps) {
|
|
157
|
+
const inputState = getRadioState(props)
|
|
158
|
+
|
|
159
|
+
const isRequired = props.required
|
|
160
|
+
const trulyDisabled = inputState.isDisabled && !props.focusable
|
|
161
|
+
|
|
162
|
+
return normalize.input({
|
|
163
|
+
...parts.radioInput.attrs,
|
|
164
|
+
"data-ownedby": dom.getRootId(state.context),
|
|
165
|
+
id: dom.getRadioInputId(state.context, props.value),
|
|
166
|
+
type: "radio",
|
|
167
|
+
name: state.context.name || state.context.id,
|
|
168
|
+
form: state.context.form,
|
|
169
|
+
value: props.value,
|
|
170
|
+
onChange(event) {
|
|
171
|
+
if (inputState.isReadOnly || inputState.isDisabled) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (event.target.checked) {
|
|
175
|
+
send({ type: "SET_VALUE", value: props.value })
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
onBlur() {
|
|
179
|
+
send({ type: "SET_FOCUSED", value: null })
|
|
180
|
+
},
|
|
181
|
+
onFocus() {
|
|
182
|
+
send({ type: "SET_FOCUSED", value: props.value, focused: true })
|
|
183
|
+
},
|
|
184
|
+
onKeyDown(event) {
|
|
185
|
+
if (event.key === " ") {
|
|
186
|
+
send({ type: "SET_ACTIVE", value: props.value, active: true })
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
onKeyUp(event) {
|
|
190
|
+
if (event.key === " ") {
|
|
191
|
+
send({ type: "SET_ACTIVE", value: null })
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
disabled: trulyDisabled,
|
|
195
|
+
required: isRequired,
|
|
196
|
+
defaultChecked: inputState.isChecked,
|
|
197
|
+
"data-disabled": dataAttr(inputState.isDisabled),
|
|
198
|
+
"aria-required": ariaAttr(isRequired),
|
|
199
|
+
"aria-invalid": ariaAttr(inputState.isInvalid),
|
|
200
|
+
readOnly: inputState.isReadOnly,
|
|
201
|
+
"data-readonly": dataAttr(inputState.isReadOnly),
|
|
202
|
+
"aria-disabled": ariaAttr(trulyDisabled),
|
|
203
|
+
"aria-checked": ariaAttr(inputState.isChecked),
|
|
204
|
+
style: visuallyHiddenStyle,
|
|
205
|
+
})
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
indicatorProps: normalize.element({
|
|
209
|
+
id: dom.getIndicatorId(state.context),
|
|
210
|
+
...parts.indicator.attrs,
|
|
211
|
+
"data-orientation": state.context.orientation,
|
|
212
|
+
style: {
|
|
213
|
+
"--transition-duration": "150ms",
|
|
214
|
+
"--transition-property": "left, top, width, height",
|
|
215
|
+
position: "absolute",
|
|
216
|
+
willChange: "var(--transition-property)",
|
|
217
|
+
transitionProperty: "var(--transition-property)",
|
|
218
|
+
transitionDuration: state.context.canIndicatorTransition ? "var(--transition-duration)" : "0ms",
|
|
219
|
+
transitionTimingFunction: "var(--transition-timing-function)",
|
|
220
|
+
...state.context.indicatorRect,
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createScope, queryAll } from "@zag-js/dom-query"
|
|
2
|
+
import type { MachineContext as Ctx } from "./radio-group.types"
|
|
3
|
+
|
|
4
|
+
export const dom = createScope({
|
|
5
|
+
getRootId: (ctx: Ctx) => ctx.ids?.root ?? `radio-group:${ctx.id}`,
|
|
6
|
+
getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `radio-group:${ctx.id}:label`,
|
|
7
|
+
getRadioId: (ctx: Ctx, value: string) => ctx.ids?.radio?.(value) ?? `radio-group:${ctx.id}:radio:${value}`,
|
|
8
|
+
getRadioInputId: (ctx: Ctx, value: string) =>
|
|
9
|
+
ctx.ids?.radioInput?.(value) ?? `radio-group:${ctx.id}:radio:input:${value}`,
|
|
10
|
+
getRadioControlId: (ctx: Ctx, value: string) =>
|
|
11
|
+
ctx.ids?.radioControl?.(value) ?? `radio-group:${ctx.id}:radio:control:${value}`,
|
|
12
|
+
getRadioLabelId: (ctx: Ctx, value: string) =>
|
|
13
|
+
ctx.ids?.radioLabel?.(value) ?? `radio-group:${ctx.id}:radio:label:${value}`,
|
|
14
|
+
getIndicatorId: (ctx: Ctx) => ctx.ids?.indicator ?? `tabs:${ctx.id}:indicator`,
|
|
15
|
+
|
|
16
|
+
getRootEl: (ctx: Ctx) => dom.getById(ctx, dom.getRootId(ctx)),
|
|
17
|
+
getRadioInputEl: (ctx: Ctx, value: string) => dom.getById<HTMLInputElement>(ctx, dom.getRadioInputId(ctx, value)),
|
|
18
|
+
getIndicatorEl: (ctx: Ctx) => dom.getById(ctx, dom.getIndicatorId(ctx)),
|
|
19
|
+
|
|
20
|
+
getFirstEnabledInputEl: (ctx: Ctx) => dom.getRootEl(ctx)?.querySelector<HTMLInputElement>("input:not(:disabled)"),
|
|
21
|
+
getFirstEnabledAndCheckedInputEl: (ctx: Ctx) =>
|
|
22
|
+
dom.getRootEl(ctx)?.querySelector<HTMLInputElement>("input:not(:disabled):checked"),
|
|
23
|
+
|
|
24
|
+
getInputEls: (ctx: Ctx) => {
|
|
25
|
+
const ownerId = CSS.escape(dom.getRootId(ctx))
|
|
26
|
+
const selector = `input[type=radio][data-ownedby='${ownerId}']:not([disabled])`
|
|
27
|
+
return queryAll<HTMLInputElement>(dom.getRootEl(ctx), selector)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getActiveRadioEl: (ctx: Ctx) => {
|
|
31
|
+
if (!ctx.value) return
|
|
32
|
+
return dom.getById(ctx, dom.getRadioId(ctx, ctx.value))
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getOffsetRect: (el: HTMLElement | undefined) => {
|
|
36
|
+
return {
|
|
37
|
+
left: el?.offsetLeft ?? 0,
|
|
38
|
+
top: el?.offsetTop ?? 0,
|
|
39
|
+
width: el?.offsetWidth ?? 0,
|
|
40
|
+
height: el?.offsetHeight ?? 0,
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getRectById: (ctx: Ctx, id: string) => {
|
|
45
|
+
const tab = dom.queryById(ctx, dom.getRadioId(ctx, id))
|
|
46
|
+
return dom.resolveRect(dom.getOffsetRect(tab))
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
resolveRect(rect: Record<"width" | "height" | "left" | "top", number>) {
|
|
50
|
+
return {
|
|
51
|
+
width: `${rect.width}px`,
|
|
52
|
+
height: `${rect.height}px`,
|
|
53
|
+
left: `${rect.left}px`,
|
|
54
|
+
top: `${rect.top}px`,
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createMachine } from "@zag-js/core"
|
|
2
|
+
import { nextTick } from "@zag-js/dom-query"
|
|
3
|
+
import { trackElementRect } from "@zag-js/element-rect"
|
|
4
|
+
import { trackFormControl } from "@zag-js/form-utils"
|
|
5
|
+
import { compact, isString } from "@zag-js/utils"
|
|
6
|
+
import { dom } from "./radio-group.dom"
|
|
7
|
+
import type { MachineContext, MachineState, UserDefinedContext } from "./radio-group.types"
|
|
8
|
+
|
|
9
|
+
export function machine(userContext: UserDefinedContext) {
|
|
10
|
+
const ctx = compact(userContext)
|
|
11
|
+
return createMachine<MachineContext, MachineState>(
|
|
12
|
+
{
|
|
13
|
+
id: "radio",
|
|
14
|
+
initial: "idle",
|
|
15
|
+
context: {
|
|
16
|
+
previousValue: null,
|
|
17
|
+
value: null,
|
|
18
|
+
activeId: null,
|
|
19
|
+
focusedId: null,
|
|
20
|
+
hoveredId: null,
|
|
21
|
+
indicatorRect: {},
|
|
22
|
+
canIndicatorTransition: false,
|
|
23
|
+
...ctx,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
entry: ["syncIndicatorRect"],
|
|
27
|
+
|
|
28
|
+
exit: ["cleanupObserver"],
|
|
29
|
+
|
|
30
|
+
activities: ["trackFormControlState"],
|
|
31
|
+
|
|
32
|
+
watch: {
|
|
33
|
+
value: [
|
|
34
|
+
"setIndicatorTransition",
|
|
35
|
+
// important to set this after `setIndicatorTransition`
|
|
36
|
+
"setPreviousValue",
|
|
37
|
+
"invokeOnChange",
|
|
38
|
+
"syncIndicatorRect",
|
|
39
|
+
"syncInputElements",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
on: {
|
|
44
|
+
SET_VALUE: {
|
|
45
|
+
actions: ["setValue"],
|
|
46
|
+
},
|
|
47
|
+
SET_HOVERED: {
|
|
48
|
+
actions: "setHovered",
|
|
49
|
+
},
|
|
50
|
+
SET_ACTIVE: {
|
|
51
|
+
actions: "setActive",
|
|
52
|
+
},
|
|
53
|
+
SET_FOCUSED: {
|
|
54
|
+
actions: "setFocused",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
states: {
|
|
59
|
+
idle: {},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
activities: {
|
|
65
|
+
trackFormControlState(ctx, _evt, { send, initialContext }) {
|
|
66
|
+
return trackFormControl(dom.getRootEl(ctx), {
|
|
67
|
+
onFieldsetDisabled() {
|
|
68
|
+
ctx.disabled = true
|
|
69
|
+
},
|
|
70
|
+
onFormReset() {
|
|
71
|
+
send({ type: "SET_VALUE", value: initialContext.value })
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
actions: {
|
|
78
|
+
setValue(ctx, evt) {
|
|
79
|
+
ctx.value = evt.value
|
|
80
|
+
},
|
|
81
|
+
setHovered(ctx, evt) {
|
|
82
|
+
ctx.hoveredId = evt.value
|
|
83
|
+
},
|
|
84
|
+
setActive(ctx, evt) {
|
|
85
|
+
ctx.activeId = evt.value
|
|
86
|
+
},
|
|
87
|
+
setFocused(ctx, evt) {
|
|
88
|
+
ctx.focusedId = evt.value
|
|
89
|
+
},
|
|
90
|
+
invokeOnChange(ctx, evt) {
|
|
91
|
+
ctx.onChange?.({ value: evt.value })
|
|
92
|
+
},
|
|
93
|
+
syncInputElements(ctx) {
|
|
94
|
+
const inputs = dom.getInputEls(ctx)
|
|
95
|
+
inputs.forEach((input) => {
|
|
96
|
+
input.checked = input.value === ctx.value
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
setPreviousValue(ctx) {
|
|
100
|
+
ctx.previousValue = ctx.value
|
|
101
|
+
},
|
|
102
|
+
setIndicatorTransition(ctx) {
|
|
103
|
+
ctx.canIndicatorTransition = isString(ctx.previousValue) && isString(ctx.value)
|
|
104
|
+
},
|
|
105
|
+
cleanupObserver(ctx) {
|
|
106
|
+
ctx.indicatorCleanup?.()
|
|
107
|
+
},
|
|
108
|
+
syncIndicatorRect(ctx) {
|
|
109
|
+
ctx.indicatorCleanup?.()
|
|
110
|
+
|
|
111
|
+
if (!dom.getIndicatorEl(ctx)) return
|
|
112
|
+
|
|
113
|
+
const value = ctx.value
|
|
114
|
+
|
|
115
|
+
if (value == null) {
|
|
116
|
+
ctx.indicatorRect = {}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const radioEl = dom.getActiveRadioEl(ctx)
|
|
121
|
+
if (!radioEl) return
|
|
122
|
+
|
|
123
|
+
ctx.indicatorCleanup = trackElementRect(radioEl, {
|
|
124
|
+
getRect(el) {
|
|
125
|
+
return dom.getOffsetRect(el)
|
|
126
|
+
},
|
|
127
|
+
onChange(rect) {
|
|
128
|
+
ctx.indicatorRect = dom.resolveRect(rect)
|
|
129
|
+
nextTick(() => {
|
|
130
|
+
ctx.canIndicatorTransition = false
|
|
131
|
+
})
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { StateMachine as S } from "@zag-js/core"
|
|
2
|
+
import type { CommonProperties, Context, DirectionProperty, RequiredBy } from "@zag-js/types"
|
|
3
|
+
|
|
4
|
+
type ElementIds = Partial<{
|
|
5
|
+
root: string
|
|
6
|
+
label: string
|
|
7
|
+
indicator: string
|
|
8
|
+
radio(value: string): string
|
|
9
|
+
radioLabel(value: string): string
|
|
10
|
+
radioControl(value: string): string
|
|
11
|
+
radioInput(value: string): string
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
type PublicContext = DirectionProperty &
|
|
15
|
+
CommonProperties & {
|
|
16
|
+
/**
|
|
17
|
+
* The ids of the elements in the radio. Useful for composition.
|
|
18
|
+
*/
|
|
19
|
+
ids?: ElementIds
|
|
20
|
+
/**
|
|
21
|
+
* The value of the checked radio
|
|
22
|
+
*/
|
|
23
|
+
value: string | null
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The name of the input fields in the radio
|
|
27
|
+
* (Useful for form submission).
|
|
28
|
+
*/
|
|
29
|
+
name?: string
|
|
30
|
+
/**
|
|
31
|
+
* The associate form of the underlying input.
|
|
32
|
+
*/
|
|
33
|
+
form?: string
|
|
34
|
+
/**
|
|
35
|
+
* If `true`, the radio group will be disabled
|
|
36
|
+
*/
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
/**
|
|
39
|
+
* If `true`, the radio group will be readonly
|
|
40
|
+
*/
|
|
41
|
+
readOnly?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Function called once a radio is checked
|
|
44
|
+
* @param value the value of the checked radio
|
|
45
|
+
*/
|
|
46
|
+
onChange?(details: { value: string }): void
|
|
47
|
+
/**
|
|
48
|
+
* Orientation of the radio group
|
|
49
|
+
*/
|
|
50
|
+
orientation?: "horizontal" | "vertical"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type PrivateContext = Context<{
|
|
54
|
+
/**
|
|
55
|
+
* @internal
|
|
56
|
+
* The id of the active radio
|
|
57
|
+
*/
|
|
58
|
+
activeId: string | null
|
|
59
|
+
/**
|
|
60
|
+
* @internal
|
|
61
|
+
* The id of the focused radio
|
|
62
|
+
*/
|
|
63
|
+
focusedId: string | null
|
|
64
|
+
/**
|
|
65
|
+
* @internal
|
|
66
|
+
* The id of the hovered radio
|
|
67
|
+
*/
|
|
68
|
+
hoveredId: string | null
|
|
69
|
+
/**
|
|
70
|
+
* @internal
|
|
71
|
+
* The active tab indicator's dom rect
|
|
72
|
+
*/
|
|
73
|
+
indicatorRect?: Partial<{ left: string; top: string; width: string; height: string }>
|
|
74
|
+
/**
|
|
75
|
+
* @internal
|
|
76
|
+
* Whether the active tab indicator's rect can transition
|
|
77
|
+
*/
|
|
78
|
+
canIndicatorTransition?: boolean
|
|
79
|
+
/**
|
|
80
|
+
* @internal
|
|
81
|
+
* Function to clean up the observer for the active tab's rect
|
|
82
|
+
*/
|
|
83
|
+
indicatorCleanup?: VoidFunction | null
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
* The previous value of the radio group
|
|
87
|
+
*/
|
|
88
|
+
previousValue: string | null
|
|
89
|
+
}>
|
|
90
|
+
|
|
91
|
+
export type UserDefinedContext = RequiredBy<PublicContext, "id">
|
|
92
|
+
|
|
93
|
+
type ComputedContext = Readonly<{}>
|
|
94
|
+
|
|
95
|
+
export type MachineContext = PublicContext & PrivateContext & ComputedContext
|
|
96
|
+
|
|
97
|
+
export type MachineState = {
|
|
98
|
+
value: "idle"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type State = S.State<MachineContext, MachineState>
|
|
102
|
+
|
|
103
|
+
export type Send = S.Send<S.AnyEventObject>
|
|
104
|
+
|
|
105
|
+
export type RadioProps = {
|
|
106
|
+
value: string
|
|
107
|
+
/**
|
|
108
|
+
* If `true`, the radio will be disabled
|
|
109
|
+
*/
|
|
110
|
+
disabled?: boolean
|
|
111
|
+
/**
|
|
112
|
+
* If `true`, the radio will be readonly
|
|
113
|
+
*/
|
|
114
|
+
readOnly?: boolean
|
|
115
|
+
/**
|
|
116
|
+
* If `true`, the radio is marked as invalid.
|
|
117
|
+
*/
|
|
118
|
+
invalid?: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type InputProps = RadioProps & {
|
|
122
|
+
/**
|
|
123
|
+
* If `true` and `disabled` is passed, the radio will
|
|
124
|
+
* remain tabbable but not interactive
|
|
125
|
+
*/
|
|
126
|
+
focusable?: boolean
|
|
127
|
+
/**
|
|
128
|
+
* If `true`, the radio input is marked as required,
|
|
129
|
+
*/
|
|
130
|
+
required?: boolean
|
|
131
|
+
}
|