@zag-js/pin-input 0.22.0 → 0.23.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.
- package/dist/index.js +85 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/pin-input.connect.ts +36 -24
- package/src/pin-input.machine.ts +40 -76
- package/src/pin-input.utils.ts +18 -0
package/src/pin-input.connect.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getEventKey, getNativeEvent, isModifiedEvent, type EventKeyMap } from "@zag-js/dom-event"
|
|
2
|
-
import { ariaAttr, dataAttr } from "@zag-js/dom-query"
|
|
2
|
+
import { ariaAttr, dataAttr, getBeforeInputValue } from "@zag-js/dom-query"
|
|
3
3
|
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
4
4
|
import { invariant } from "@zag-js/utils"
|
|
5
5
|
import { visuallyHiddenStyle } from "@zag-js/visually-hidden"
|
|
6
6
|
import { parts } from "./pin-input.anatomy"
|
|
7
7
|
import { dom } from "./pin-input.dom"
|
|
8
8
|
import type { MachineApi, Send, State } from "./pin-input.types"
|
|
9
|
+
import { isValidValue } from "./pin-input.utils"
|
|
9
10
|
|
|
10
11
|
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
|
|
11
12
|
const isValueComplete = state.context.isValueComplete
|
|
@@ -22,19 +23,19 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
|
|
|
22
23
|
valueAsString: state.context.valueAsString,
|
|
23
24
|
isValueComplete: isValueComplete,
|
|
24
25
|
|
|
25
|
-
setValue(value
|
|
26
|
+
setValue(value) {
|
|
26
27
|
if (!Array.isArray(value)) {
|
|
27
28
|
invariant("[pin-input/setValue] value must be an array")
|
|
28
29
|
}
|
|
29
|
-
send({ type: "
|
|
30
|
+
send({ type: "VALUE.SET", value })
|
|
30
31
|
},
|
|
31
32
|
|
|
32
33
|
clearValue() {
|
|
33
|
-
send({ type: "
|
|
34
|
+
send({ type: "VALUE.CLEAR" })
|
|
34
35
|
},
|
|
35
36
|
|
|
36
|
-
setValueAtIndex(index
|
|
37
|
-
send({ type: "
|
|
37
|
+
setValueAtIndex(index, value) {
|
|
38
|
+
send({ type: "VALUE.SET", value, index })
|
|
38
39
|
},
|
|
39
40
|
|
|
40
41
|
focus,
|
|
@@ -56,7 +57,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
|
|
|
56
57
|
"data-invalid": dataAttr(isInvalid),
|
|
57
58
|
"data-disabled": dataAttr(state.context.disabled),
|
|
58
59
|
"data-complete": dataAttr(isValueComplete),
|
|
59
|
-
onClick
|
|
60
|
+
onClick(event) {
|
|
60
61
|
event.preventDefault()
|
|
61
62
|
focus()
|
|
62
63
|
},
|
|
@@ -80,7 +81,8 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
|
|
|
80
81
|
id: dom.getControlId(state.context),
|
|
81
82
|
}),
|
|
82
83
|
|
|
83
|
-
getInputProps(
|
|
84
|
+
getInputProps(props) {
|
|
85
|
+
const { index } = props
|
|
84
86
|
const inputType = state.context.type === "numeric" ? "tel" : "text"
|
|
85
87
|
return normalize.input({
|
|
86
88
|
...parts.input.attrs,
|
|
@@ -99,59 +101,69 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
|
|
|
99
101
|
autoCapitalize: "none",
|
|
100
102
|
autoComplete: state.context.otp ? "one-time-code" : "off",
|
|
101
103
|
placeholder: focusedIndex === index ? "" : state.context.placeholder,
|
|
104
|
+
onBeforeInput(event) {
|
|
105
|
+
try {
|
|
106
|
+
const value = getBeforeInputValue(event)
|
|
107
|
+
const isValid = isValidValue(state.context, value)
|
|
108
|
+
if (!isValid) {
|
|
109
|
+
send({ type: "VALUE.INVALID", value })
|
|
110
|
+
event.preventDefault()
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// noop
|
|
114
|
+
}
|
|
115
|
+
},
|
|
102
116
|
onChange(event) {
|
|
103
117
|
const evt = getNativeEvent(event)
|
|
104
118
|
const { value } = event.currentTarget
|
|
119
|
+
|
|
105
120
|
if (evt.inputType === "insertFromPaste" || value.length > 2) {
|
|
106
|
-
send({ type: "PASTE", value })
|
|
121
|
+
send({ type: "INPUT.PASTE", value })
|
|
107
122
|
event.preventDefault()
|
|
108
123
|
return
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
if (evt.inputType === "deleteContentBackward") {
|
|
112
|
-
send("BACKSPACE")
|
|
127
|
+
send("INPUT.BACKSPACE")
|
|
113
128
|
return
|
|
114
129
|
}
|
|
115
|
-
|
|
130
|
+
|
|
131
|
+
send({ type: "INPUT.CHANGE", value, index })
|
|
116
132
|
},
|
|
117
133
|
onKeyDown(event) {
|
|
118
134
|
const evt = getNativeEvent(event)
|
|
119
|
-
if (
|
|
135
|
+
if (isModifiedEvent(evt)) return
|
|
120
136
|
|
|
121
137
|
const keyMap: EventKeyMap = {
|
|
122
138
|
Backspace() {
|
|
123
|
-
send("BACKSPACE")
|
|
139
|
+
send("INPUT.BACKSPACE")
|
|
124
140
|
},
|
|
125
141
|
Delete() {
|
|
126
|
-
send("DELETE")
|
|
142
|
+
send("INPUT.DELETE")
|
|
127
143
|
},
|
|
128
144
|
ArrowLeft() {
|
|
129
|
-
send("ARROW_LEFT")
|
|
145
|
+
send("INPUT.ARROW_LEFT")
|
|
130
146
|
},
|
|
131
147
|
ArrowRight() {
|
|
132
|
-
send("ARROW_RIGHT")
|
|
148
|
+
send("INPUT.ARROW_RIGHT")
|
|
133
149
|
},
|
|
134
150
|
Enter() {
|
|
135
|
-
send("ENTER")
|
|
151
|
+
send("INPUT.ENTER")
|
|
136
152
|
},
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
const
|
|
140
|
-
const exec = keyMap[key]
|
|
155
|
+
const exec = keyMap[getEventKey(event, state.context)]
|
|
141
156
|
|
|
142
157
|
if (exec) {
|
|
143
158
|
exec(event)
|
|
144
159
|
event.preventDefault()
|
|
145
|
-
} else {
|
|
146
|
-
if (key === "Tab") return
|
|
147
|
-
send({ type: "KEY_DOWN", value: key, preventDefault: () => event.preventDefault() })
|
|
148
160
|
}
|
|
149
161
|
},
|
|
150
162
|
onFocus() {
|
|
151
|
-
send({ type: "FOCUS", index })
|
|
163
|
+
send({ type: "INPUT.FOCUS", index })
|
|
152
164
|
},
|
|
153
165
|
onBlur() {
|
|
154
|
-
send({ type: "BLUR", index })
|
|
166
|
+
send({ type: "INPUT.BLUR", index })
|
|
155
167
|
},
|
|
156
168
|
})
|
|
157
169
|
},
|
package/src/pin-input.machine.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { choose, createMachine } from "@zag-js/core"
|
|
2
2
|
import { raf } from "@zag-js/dom-query"
|
|
3
3
|
import { dispatchInputValueEvent } from "@zag-js/form-utils"
|
|
4
4
|
import { compact, isEqual } from "@zag-js/utils"
|
|
5
5
|
import { dom } from "./pin-input.dom"
|
|
6
6
|
import type { MachineContext, MachineState, UserDefinedContext } from "./pin-input.types"
|
|
7
7
|
|
|
8
|
-
const { and, not } = guards
|
|
9
|
-
|
|
10
8
|
export function machine(userContext: UserDefinedContext) {
|
|
11
9
|
const ctx = compact(userContext)
|
|
12
10
|
return createMachine<MachineContext, MachineState>(
|
|
13
11
|
{
|
|
14
12
|
id: "pin-input",
|
|
15
|
-
initial:
|
|
13
|
+
initial: "idle",
|
|
16
14
|
context: {
|
|
17
15
|
value: [],
|
|
18
16
|
focusedIndex: -1,
|
|
@@ -31,40 +29,40 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
31
29
|
filledValueLength: (ctx) => ctx.value.filter((v) => v?.trim() !== "").length,
|
|
32
30
|
isValueComplete: (ctx) => ctx.valueLength === ctx.filledValueLength,
|
|
33
31
|
valueAsString: (ctx) => ctx.value.join(""),
|
|
34
|
-
focusedValue: (ctx) => ctx.value[ctx.focusedIndex],
|
|
32
|
+
focusedValue: (ctx) => ctx.value[ctx.focusedIndex] || "",
|
|
35
33
|
},
|
|
36
34
|
|
|
35
|
+
entry: choose([
|
|
36
|
+
{
|
|
37
|
+
guard: "autoFocus",
|
|
38
|
+
actions: ["setupValue", "setFocusIndexToFirst"],
|
|
39
|
+
},
|
|
40
|
+
{ actions: ["setupValue"] },
|
|
41
|
+
]),
|
|
42
|
+
|
|
37
43
|
watch: {
|
|
38
44
|
focusedIndex: ["focusInput", "selectInputIfNeeded"],
|
|
39
45
|
value: ["syncInputElements"],
|
|
40
46
|
isValueComplete: ["invokeOnComplete", "blurFocusedInputIfNeeded"],
|
|
41
47
|
},
|
|
42
48
|
|
|
43
|
-
entry: ctx.autoFocus ? ["setupValue", "setFocusIndexToFirst"] : ["setupValue"],
|
|
44
|
-
|
|
45
49
|
on: {
|
|
46
|
-
|
|
50
|
+
"VALUE.SET": [
|
|
47
51
|
{
|
|
48
52
|
guard: "hasIndex",
|
|
49
53
|
actions: ["setValueAtIndex"],
|
|
50
54
|
},
|
|
51
55
|
{ actions: ["setValue"] },
|
|
52
56
|
],
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
actions: ["clearValue"],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
actions: ["clearValue", "setFocusIndexToFirst"],
|
|
60
|
-
},
|
|
61
|
-
],
|
|
57
|
+
"VALUE.CLEAR": {
|
|
58
|
+
actions: ["clearValue", "setFocusIndexToFirst"],
|
|
59
|
+
},
|
|
62
60
|
},
|
|
63
61
|
|
|
64
62
|
states: {
|
|
65
63
|
idle: {
|
|
66
64
|
on: {
|
|
67
|
-
FOCUS: {
|
|
65
|
+
"INPUT.FOCUS": {
|
|
68
66
|
target: "focused",
|
|
69
67
|
actions: "setFocusedIndex",
|
|
70
68
|
},
|
|
@@ -72,38 +70,33 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
72
70
|
},
|
|
73
71
|
focused: {
|
|
74
72
|
on: {
|
|
75
|
-
INPUT: [
|
|
73
|
+
"INPUT.CHANGE": [
|
|
76
74
|
{
|
|
77
|
-
guard:
|
|
75
|
+
guard: "isFinalValue",
|
|
78
76
|
actions: ["setFocusedValue", "syncInputValue"],
|
|
79
77
|
},
|
|
80
78
|
{
|
|
81
|
-
guard: "isValidValue",
|
|
82
79
|
actions: ["setFocusedValue", "setNextFocusedIndex", "syncInputValue"],
|
|
83
80
|
},
|
|
84
81
|
],
|
|
85
|
-
PASTE:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
{ actions: ["revertInputValue"] },
|
|
91
|
-
],
|
|
92
|
-
BLUR: {
|
|
82
|
+
"INPUT.PASTE": {
|
|
83
|
+
actions: ["setPastedValue", "setLastValueFocusIndex"],
|
|
84
|
+
},
|
|
85
|
+
"INPUT.BLUR": {
|
|
93
86
|
target: "idle",
|
|
94
87
|
actions: "clearFocusedIndex",
|
|
95
88
|
},
|
|
96
|
-
DELETE: {
|
|
89
|
+
"INPUT.DELETE": {
|
|
97
90
|
guard: "hasValue",
|
|
98
|
-
actions:
|
|
91
|
+
actions: "clearFocusedValue",
|
|
99
92
|
},
|
|
100
|
-
ARROW_LEFT: {
|
|
93
|
+
"INPUT.ARROW_LEFT": {
|
|
101
94
|
actions: "setPrevFocusedIndex",
|
|
102
95
|
},
|
|
103
|
-
ARROW_RIGHT: {
|
|
96
|
+
"INPUT.ARROW_RIGHT": {
|
|
104
97
|
actions: "setNextFocusedIndex",
|
|
105
98
|
},
|
|
106
|
-
BACKSPACE: [
|
|
99
|
+
"INPUT.BACKSPACE": [
|
|
107
100
|
{
|
|
108
101
|
guard: "hasValue",
|
|
109
102
|
actions: ["clearFocusedValue"],
|
|
@@ -112,13 +105,12 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
112
105
|
actions: ["setPrevFocusedIndex", "clearFocusedValue"],
|
|
113
106
|
},
|
|
114
107
|
],
|
|
115
|
-
ENTER: {
|
|
108
|
+
"INPUT.ENTER": {
|
|
116
109
|
guard: "isValueComplete",
|
|
117
110
|
actions: "requestFormSubmit",
|
|
118
111
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
actions: ["preventDefault", "invokeOnInvalid"],
|
|
112
|
+
"VALUE.INVALID": {
|
|
113
|
+
actions: "invokeOnInvalid",
|
|
122
114
|
},
|
|
123
115
|
},
|
|
124
116
|
},
|
|
@@ -130,41 +122,27 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
130
122
|
isValueEmpty: (_ctx, evt) => evt.value === "",
|
|
131
123
|
hasValue: (ctx) => ctx.value[ctx.focusedIndex] !== "",
|
|
132
124
|
isValueComplete: (ctx) => ctx.isValueComplete,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return regex.test(evt.value)
|
|
137
|
-
},
|
|
138
|
-
isFinalValue(ctx) {
|
|
139
|
-
return (
|
|
140
|
-
ctx.filledValueLength + 1 === ctx.valueLength &&
|
|
141
|
-
ctx.value.findIndex((v) => v.trim() === "") === ctx.focusedIndex
|
|
142
|
-
)
|
|
143
|
-
},
|
|
144
|
-
isLastInputFocused: (ctx) => ctx.focusedIndex === ctx.valueLength - 1,
|
|
125
|
+
isFinalValue: (ctx) =>
|
|
126
|
+
ctx.filledValueLength + 1 === ctx.valueLength &&
|
|
127
|
+
ctx.value.findIndex((v) => v.trim() === "") === ctx.focusedIndex,
|
|
145
128
|
hasIndex: (_ctx, evt) => evt.index !== undefined,
|
|
146
129
|
isDisabled: (ctx) => !!ctx.disabled,
|
|
147
130
|
},
|
|
148
131
|
actions: {
|
|
149
132
|
setupValue(ctx) {
|
|
150
133
|
if (ctx.value.length) return
|
|
151
|
-
const
|
|
152
|
-
const emptyValues = Array.from<string>({ length:
|
|
134
|
+
const inputEls = dom.getInputEls(ctx)
|
|
135
|
+
const emptyValues = Array.from<string>({ length: inputEls.length }).fill("")
|
|
153
136
|
assignValue(ctx, emptyValues)
|
|
154
137
|
},
|
|
155
138
|
focusInput(ctx) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
dom.getFocusedInputEl(ctx)?.focus()
|
|
159
|
-
})
|
|
139
|
+
if (ctx.focusedIndex === -1) return
|
|
140
|
+
dom.getFocusedInputEl(ctx)?.focus({ preventScroll: true })
|
|
160
141
|
},
|
|
161
142
|
selectInputIfNeeded(ctx) {
|
|
143
|
+
if (!ctx.selectOnFocus || ctx.focusedIndex === -1) return
|
|
162
144
|
raf(() => {
|
|
163
|
-
|
|
164
|
-
const input = dom.getFocusedInputEl(ctx)
|
|
165
|
-
const length = input.value.length
|
|
166
|
-
input.selectionStart = ctx.selectOnFocus ? 0 : length
|
|
167
|
-
input.selectionEnd = length
|
|
145
|
+
dom.getFocusedInputEl(ctx)?.select()
|
|
168
146
|
})
|
|
169
147
|
},
|
|
170
148
|
invokeOnComplete(ctx) {
|
|
@@ -239,9 +217,6 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
239
217
|
ctx.focusedIndex = Math.min(ctx.filledValueLength, ctx.valueLength - 1)
|
|
240
218
|
})
|
|
241
219
|
},
|
|
242
|
-
preventDefault(_, evt) {
|
|
243
|
-
evt.preventDefault()
|
|
244
|
-
},
|
|
245
220
|
blurFocusedInputIfNeeded(ctx) {
|
|
246
221
|
if (!ctx.blurOnComplete) return
|
|
247
222
|
raf(() => {
|
|
@@ -258,17 +233,6 @@ export function machine(userContext: UserDefinedContext) {
|
|
|
258
233
|
)
|
|
259
234
|
}
|
|
260
235
|
|
|
261
|
-
const REGEX = {
|
|
262
|
-
numeric: /^[0-9]+$/,
|
|
263
|
-
alphabetic: /^[A-Za-z]+$/,
|
|
264
|
-
alphanumeric: /^[a-zA-Z0-9]+$/i,
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function isValidType(value: string, type: MachineContext["type"]) {
|
|
268
|
-
if (!type) return true
|
|
269
|
-
return !!REGEX[type]?.test(value)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
236
|
function assignValue(ctx: MachineContext, value: string | string[]) {
|
|
273
237
|
const arr = Array.isArray(value) ? value : value.split("").filter(Boolean)
|
|
274
238
|
arr.forEach((value, index) => {
|
|
@@ -280,7 +244,7 @@ function getNextValue(current: string, next: string) {
|
|
|
280
244
|
let nextValue = next
|
|
281
245
|
if (current[0] === next[0]) nextValue = next[1]
|
|
282
246
|
else if (current[0] === next[1]) nextValue = next[0]
|
|
283
|
-
return nextValue
|
|
247
|
+
return nextValue.split("")[nextValue.length - 1]
|
|
284
248
|
}
|
|
285
249
|
|
|
286
250
|
const invoke = {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MachineContext } from "./pin-input.types"
|
|
2
|
+
|
|
3
|
+
const REGEX = {
|
|
4
|
+
numeric: /^[0-9]+$/,
|
|
5
|
+
alphabetic: /^[A-Za-z]+$/,
|
|
6
|
+
alphanumeric: /^[a-zA-Z0-9]+$/i,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isValidType(ctx: MachineContext, value: string) {
|
|
10
|
+
if (!ctx.type) return true
|
|
11
|
+
return !!REGEX[ctx.type]?.test(value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isValidValue(ctx: MachineContext, value: string) {
|
|
15
|
+
if (!ctx.pattern) return isValidType(ctx, value)
|
|
16
|
+
const regex = new RegExp(ctx.pattern, "g")
|
|
17
|
+
return regex.test(value)
|
|
18
|
+
}
|