@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.
@@ -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: string[]) {
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: "SET_VALUE", value })
30
+ send({ type: "VALUE.SET", value })
30
31
  },
31
32
 
32
33
  clearValue() {
33
- send({ type: "CLEAR_VALUE" })
34
+ send({ type: "VALUE.CLEAR" })
34
35
  },
35
36
 
36
- setValueAtIndex(index: number, value: string) {
37
- send({ type: "SET_VALUE", value, index })
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: (event) => {
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({ index }: { index: number }) {
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
- send({ type: "INPUT", value, index })
130
+
131
+ send({ type: "INPUT.CHANGE", value, index })
116
132
  },
117
133
  onKeyDown(event) {
118
134
  const evt = getNativeEvent(event)
119
- if (evt.isComposing || isModifiedEvent(evt)) return
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 key = getEventKey(event, { dir: state.context.dir })
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
  },
@@ -1,18 +1,16 @@
1
- import { createMachine, guards } from "@zag-js/core"
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: ctx.autoFocus ? "focused" : "idle",
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
- SET_VALUE: [
50
+ "VALUE.SET": [
47
51
  {
48
52
  guard: "hasIndex",
49
53
  actions: ["setValueAtIndex"],
50
54
  },
51
55
  { actions: ["setValue"] },
52
56
  ],
53
- CLEAR_VALUE: [
54
- {
55
- guard: "isDisabled",
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: and("isFinalValue", "isValidValue"),
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
- guard: "isValidValue",
88
- actions: ["setPastedValue", "setLastValueFocusIndex"],
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: ["clearFocusedValue"],
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
- KEY_DOWN: {
120
- guard: not("isValidValue"),
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
- isValidValue(ctx, evt) {
134
- if (!ctx.pattern) return isValidType(evt.value, ctx.type)
135
- const regex = new RegExp(ctx.pattern, "g")
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 inputs = dom.getInputEls(ctx)
152
- const emptyValues = Array.from<string>({ length: inputs.length }).fill("")
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
- raf(() => {
157
- if (ctx.focusedIndex === -1) return
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
- if (ctx.focusedIndex === -1) return
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
+ }