@zag-js/number-input 0.9.2 → 0.10.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/{chunk-ZMYZZUU6.mjs → chunk-5ENKYWE3.mjs} +0 -3
- package/dist/index.js +0 -3
- package/dist/index.mjs +1 -1
- package/dist/number-input.connect.js +0 -3
- package/dist/number-input.connect.mjs +1 -1
- package/package.json +12 -11
- package/src/index.ts +4 -0
- package/src/number-input.anatomy.ts +12 -0
- package/src/number-input.connect.ts +248 -0
- package/src/number-input.dom.ts +113 -0
- package/src/number-input.machine.ts +396 -0
- package/src/number-input.types.ts +234 -0
- package/src/number-input.utils.ts +42 -0
|
@@ -139,9 +139,6 @@ function connect(state, send, normalize) {
|
|
|
139
139
|
send("BLUR");
|
|
140
140
|
},
|
|
141
141
|
onChange(event) {
|
|
142
|
-
const evt = getNativeEvent(event);
|
|
143
|
-
if (evt.isComposing)
|
|
144
|
-
return;
|
|
145
142
|
send({ type: "CHANGE", target: event.currentTarget, hint: "set" });
|
|
146
143
|
},
|
|
147
144
|
onKeyDown(event) {
|
package/dist/index.js
CHANGED
|
@@ -310,9 +310,6 @@ function connect(state, send, normalize) {
|
|
|
310
310
|
send("BLUR");
|
|
311
311
|
},
|
|
312
312
|
onChange(event) {
|
|
313
|
-
const evt = (0, import_dom_event2.getNativeEvent)(event);
|
|
314
|
-
if (evt.isComposing)
|
|
315
|
-
return;
|
|
316
313
|
send({ type: "CHANGE", target: event.currentTarget, hint: "set" });
|
|
317
314
|
},
|
|
318
315
|
onKeyDown(event) {
|
package/dist/index.mjs
CHANGED
|
@@ -306,9 +306,6 @@ function connect(state, send, normalize) {
|
|
|
306
306
|
send("BLUR");
|
|
307
307
|
},
|
|
308
308
|
onChange(event) {
|
|
309
|
-
const evt = (0, import_dom_event2.getNativeEvent)(event);
|
|
310
|
-
if (evt.isComposing)
|
|
311
|
-
return;
|
|
312
309
|
send({ type: "CHANGE", target: event.currentTarget, hint: "set" });
|
|
313
310
|
},
|
|
314
311
|
onKeyDown(event) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/number-input",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Core logic for the number-input widget implemented as a state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/number-input",
|
|
18
18
|
"sideEffects": false,
|
|
19
19
|
"files": [
|
|
20
|
-
"dist
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
21
22
|
],
|
|
22
23
|
"publishConfig": {
|
|
23
24
|
"access": "public"
|
|
@@ -26,15 +27,15 @@
|
|
|
26
27
|
"url": "https://github.com/chakra-ui/zag/issues"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@zag-js/anatomy": "0.
|
|
30
|
-
"@zag-js/core": "0.
|
|
31
|
-
"@zag-js/dom-query": "0.
|
|
32
|
-
"@zag-js/dom-event": "0.
|
|
33
|
-
"@zag-js/form-utils": "0.
|
|
34
|
-
"@zag-js/mutation-observer": "0.
|
|
35
|
-
"@zag-js/number-utils": "0.
|
|
36
|
-
"@zag-js/utils": "0.
|
|
37
|
-
"@zag-js/types": "0.
|
|
30
|
+
"@zag-js/anatomy": "0.10.0",
|
|
31
|
+
"@zag-js/core": "0.10.0",
|
|
32
|
+
"@zag-js/dom-query": "0.10.0",
|
|
33
|
+
"@zag-js/dom-event": "0.10.0",
|
|
34
|
+
"@zag-js/form-utils": "0.10.0",
|
|
35
|
+
"@zag-js/mutation-observer": "0.10.0",
|
|
36
|
+
"@zag-js/number-utils": "0.10.0",
|
|
37
|
+
"@zag-js/utils": "0.10.0",
|
|
38
|
+
"@zag-js/types": "0.10.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"clean-package": "2.2.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createAnatomy } from "@zag-js/anatomy"
|
|
2
|
+
|
|
3
|
+
export const anatomy = createAnatomy("numberInput").parts(
|
|
4
|
+
"root",
|
|
5
|
+
"label",
|
|
6
|
+
"input",
|
|
7
|
+
"control",
|
|
8
|
+
"incrementTrigger",
|
|
9
|
+
"decrementTrigger",
|
|
10
|
+
"scrubber",
|
|
11
|
+
)
|
|
12
|
+
export const parts = anatomy.build()
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { EventKeyMap, getEventPoint, getEventStep, getNativeEvent, isLeftClick } from "@zag-js/dom-event"
|
|
2
|
+
import { ariaAttr, dataAttr } from "@zag-js/dom-query"
|
|
3
|
+
import { roundToDevicePixel } from "@zag-js/number-utils"
|
|
4
|
+
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
5
|
+
import { parts } from "./number-input.anatomy"
|
|
6
|
+
import { dom } from "./number-input.dom"
|
|
7
|
+
import type { Send, State } from "./number-input.types"
|
|
8
|
+
import { utils } from "./number-input.utils"
|
|
9
|
+
|
|
10
|
+
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
|
|
11
|
+
const isFocused = state.hasTag("focus")
|
|
12
|
+
const isInvalid = state.context.isOutOfRange || !!state.context.invalid
|
|
13
|
+
|
|
14
|
+
const isDisabled = !!state.context.disabled
|
|
15
|
+
const isValueEmpty = state.context.isValueEmpty
|
|
16
|
+
const isIncrementDisabled = isDisabled || !state.context.canIncrement
|
|
17
|
+
const isDecrementDisabled = isDisabled || !state.context.canDecrement
|
|
18
|
+
|
|
19
|
+
const translations = state.context.translations
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Whether the input is focused.
|
|
24
|
+
*/
|
|
25
|
+
isFocused,
|
|
26
|
+
/**
|
|
27
|
+
* Whether the input is invalid.
|
|
28
|
+
*/
|
|
29
|
+
isInvalid,
|
|
30
|
+
/**
|
|
31
|
+
* Whether the input value is empty.
|
|
32
|
+
*/
|
|
33
|
+
isValueEmpty,
|
|
34
|
+
/**
|
|
35
|
+
* The formatted value of the input.
|
|
36
|
+
*/
|
|
37
|
+
value: state.context.formattedValue,
|
|
38
|
+
/**
|
|
39
|
+
* The value of the input as a number.
|
|
40
|
+
*/
|
|
41
|
+
valueAsNumber: state.context.valueAsNumber,
|
|
42
|
+
/**
|
|
43
|
+
* Function to set the value of the input.
|
|
44
|
+
*/
|
|
45
|
+
setValue(value: string | number) {
|
|
46
|
+
send({ type: "SET_VALUE", value: value.toString() })
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* Function to clear the value of the input.
|
|
50
|
+
*/
|
|
51
|
+
clearValue() {
|
|
52
|
+
send("CLEAR_VALUE")
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Function to increment the value of the input by the step.
|
|
56
|
+
*/
|
|
57
|
+
increment() {
|
|
58
|
+
send("INCREMENT")
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Function to decrement the value of the input by the step.
|
|
62
|
+
*/
|
|
63
|
+
decrement() {
|
|
64
|
+
send("DECREMENT")
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Function to set the value of the input to the max.
|
|
68
|
+
*/
|
|
69
|
+
setToMax() {
|
|
70
|
+
send({ type: "SET_VALUE", value: state.context.max })
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Function to set the value of the input to the min.
|
|
74
|
+
*/
|
|
75
|
+
setToMin() {
|
|
76
|
+
send({ type: "SET_VALUE", value: state.context.min })
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Function to focus the input.
|
|
80
|
+
*/
|
|
81
|
+
focus() {
|
|
82
|
+
dom.getInputEl(state.context)?.focus()
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Function to blur the input.
|
|
86
|
+
*/
|
|
87
|
+
blur() {
|
|
88
|
+
dom.getInputEl(state.context)?.blur()
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
rootProps: normalize.element({
|
|
92
|
+
id: dom.getRootId(state.context),
|
|
93
|
+
...parts.root.attrs,
|
|
94
|
+
"data-disabled": dataAttr(isDisabled),
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
labelProps: normalize.label({
|
|
98
|
+
...parts.label.attrs,
|
|
99
|
+
"data-disabled": dataAttr(isDisabled),
|
|
100
|
+
"data-invalid": dataAttr(isInvalid),
|
|
101
|
+
id: dom.getLabelId(state.context),
|
|
102
|
+
htmlFor: dom.getInputId(state.context),
|
|
103
|
+
}),
|
|
104
|
+
|
|
105
|
+
controlProps: normalize.element({
|
|
106
|
+
...parts.control.attrs,
|
|
107
|
+
role: "group",
|
|
108
|
+
"aria-disabled": isDisabled,
|
|
109
|
+
"data-disabled": dataAttr(isDisabled),
|
|
110
|
+
"data-invalid": dataAttr(isInvalid),
|
|
111
|
+
"aria-invalid": ariaAttr(state.context.invalid),
|
|
112
|
+
}),
|
|
113
|
+
|
|
114
|
+
inputProps: normalize.input({
|
|
115
|
+
...parts.input.attrs,
|
|
116
|
+
name: state.context.name,
|
|
117
|
+
form: state.context.form,
|
|
118
|
+
id: dom.getInputId(state.context),
|
|
119
|
+
role: "spinbutton",
|
|
120
|
+
defaultValue: state.context.formattedValue,
|
|
121
|
+
pattern: state.context.pattern,
|
|
122
|
+
inputMode: state.context.inputMode,
|
|
123
|
+
"aria-invalid": ariaAttr(isInvalid),
|
|
124
|
+
"data-invalid": dataAttr(isInvalid),
|
|
125
|
+
disabled: isDisabled,
|
|
126
|
+
"data-disabled": dataAttr(isDisabled),
|
|
127
|
+
readOnly: !!state.context.readOnly,
|
|
128
|
+
autoComplete: "off",
|
|
129
|
+
autoCorrect: "off",
|
|
130
|
+
spellCheck: "false",
|
|
131
|
+
type: "text",
|
|
132
|
+
"aria-roledescription": "numberfield",
|
|
133
|
+
"aria-valuemin": state.context.min,
|
|
134
|
+
"aria-valuemax": state.context.max,
|
|
135
|
+
"aria-valuenow": isNaN(state.context.valueAsNumber) ? undefined : state.context.valueAsNumber,
|
|
136
|
+
"aria-valuetext": state.context.valueText,
|
|
137
|
+
onFocus() {
|
|
138
|
+
send("FOCUS")
|
|
139
|
+
},
|
|
140
|
+
onBlur() {
|
|
141
|
+
send("BLUR")
|
|
142
|
+
},
|
|
143
|
+
onChange(event) {
|
|
144
|
+
send({ type: "CHANGE", target: event.currentTarget, hint: "set" })
|
|
145
|
+
},
|
|
146
|
+
onKeyDown(event) {
|
|
147
|
+
const evt = getNativeEvent(event)
|
|
148
|
+
// TODO: This blocks non-ascii characters
|
|
149
|
+
if (evt.isComposing) return
|
|
150
|
+
|
|
151
|
+
if (!utils.isValidNumericEvent(state.context, event)) {
|
|
152
|
+
event.preventDefault()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const step = getEventStep(event) * state.context.step
|
|
156
|
+
|
|
157
|
+
const keyMap: EventKeyMap = {
|
|
158
|
+
ArrowUp() {
|
|
159
|
+
send({ type: "ARROW_UP", step })
|
|
160
|
+
},
|
|
161
|
+
ArrowDown() {
|
|
162
|
+
send({ type: "ARROW_DOWN", step })
|
|
163
|
+
},
|
|
164
|
+
Home() {
|
|
165
|
+
send("HOME")
|
|
166
|
+
},
|
|
167
|
+
End() {
|
|
168
|
+
send("END")
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const exec = keyMap[event.key]
|
|
173
|
+
|
|
174
|
+
if (exec) {
|
|
175
|
+
exec(event)
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
decrementTriggerProps: normalize.button({
|
|
182
|
+
...parts.decrementTrigger.attrs,
|
|
183
|
+
id: dom.getDecrementTriggerId(state.context),
|
|
184
|
+
disabled: isDecrementDisabled,
|
|
185
|
+
"data-disabled": dataAttr(isDecrementDisabled),
|
|
186
|
+
"aria-label": translations.decrementLabel,
|
|
187
|
+
type: "button",
|
|
188
|
+
tabIndex: -1,
|
|
189
|
+
"aria-controls": dom.getInputId(state.context),
|
|
190
|
+
onPointerDown(event) {
|
|
191
|
+
if (isDecrementDisabled) return
|
|
192
|
+
send(isLeftClick(event) ? { type: "PRESS_DOWN", hint: "decrement" } : { type: "FOCUS" })
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
},
|
|
195
|
+
onPointerUp() {
|
|
196
|
+
send({ type: "PRESS_UP", hint: "decrement" })
|
|
197
|
+
},
|
|
198
|
+
onPointerLeave() {
|
|
199
|
+
if (isDecrementDisabled) return
|
|
200
|
+
send({ type: "PRESS_UP", hint: "decrement" })
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
incrementTriggerProps: normalize.button({
|
|
205
|
+
...parts.incrementTrigger.attrs,
|
|
206
|
+
id: dom.getIncrementTriggerId(state.context),
|
|
207
|
+
disabled: isIncrementDisabled,
|
|
208
|
+
"data-disabled": dataAttr(isIncrementDisabled),
|
|
209
|
+
"aria-label": translations.incrementLabel,
|
|
210
|
+
type: "button",
|
|
211
|
+
tabIndex: -1,
|
|
212
|
+
"aria-controls": dom.getInputId(state.context),
|
|
213
|
+
onPointerDown(event) {
|
|
214
|
+
if (isIncrementDisabled) return
|
|
215
|
+
send(isLeftClick(event) ? { type: "PRESS_DOWN", hint: "increment" } : { type: "FOCUS" })
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
},
|
|
218
|
+
onPointerUp() {
|
|
219
|
+
send({ type: "PRESS_UP", hint: "increment" })
|
|
220
|
+
},
|
|
221
|
+
onPointerLeave() {
|
|
222
|
+
send({ type: "PRESS_UP", hint: "increment" })
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
|
|
226
|
+
scrubberProps: normalize.element({
|
|
227
|
+
...parts.scrubber.attrs,
|
|
228
|
+
"data-disabled": dataAttr(isDisabled),
|
|
229
|
+
id: dom.getScrubberId(state.context),
|
|
230
|
+
role: "presentation",
|
|
231
|
+
onMouseDown(event) {
|
|
232
|
+
if (isDisabled) return
|
|
233
|
+
|
|
234
|
+
const evt = getNativeEvent(event)
|
|
235
|
+
|
|
236
|
+
const point = getEventPoint(evt)
|
|
237
|
+
point.x = point.x - roundToDevicePixel(7.5)
|
|
238
|
+
point.y = point.y - roundToDevicePixel(7.5)
|
|
239
|
+
|
|
240
|
+
send({ type: "PRESS_DOWN_SCRUBBER", point })
|
|
241
|
+
event.preventDefault()
|
|
242
|
+
},
|
|
243
|
+
style: {
|
|
244
|
+
cursor: isDisabled ? undefined : "ew-resize",
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createScope, isSafari, MAX_Z_INDEX } from "@zag-js/dom-query"
|
|
2
|
+
import { roundToDevicePixel, wrap } from "@zag-js/number-utils"
|
|
3
|
+
import type { MachineContext as Ctx } from "./number-input.types"
|
|
4
|
+
|
|
5
|
+
export const dom = createScope({
|
|
6
|
+
getRootId: (ctx: Ctx) => ctx.ids?.root ?? `number-input:${ctx.id}`,
|
|
7
|
+
getInputId: (ctx: Ctx) => ctx.ids?.input ?? `number-input:${ctx.id}:input`,
|
|
8
|
+
getIncrementTriggerId: (ctx: Ctx) => ctx.ids?.incrementTrigger ?? `number-input:${ctx.id}:inc`,
|
|
9
|
+
getDecrementTriggerId: (ctx: Ctx) => ctx.ids?.decrementTrigger ?? `number-input:${ctx.id}:dec`,
|
|
10
|
+
getScrubberId: (ctx: Ctx) => ctx.ids?.scrubber ?? `number-input:${ctx.id}:scrubber`,
|
|
11
|
+
getCursorId: (ctx: Ctx) => `number-input:${ctx.id}:cursor`,
|
|
12
|
+
getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `number-input:${ctx.id}:label`,
|
|
13
|
+
|
|
14
|
+
getInputEl: (ctx: Ctx) => dom.getById<HTMLInputElement>(ctx, dom.getInputId(ctx)),
|
|
15
|
+
getIncrementTriggerEl: (ctx: Ctx) => dom.getById<HTMLButtonElement>(ctx, dom.getIncrementTriggerId(ctx)),
|
|
16
|
+
getDecrementTriggerEl: (ctx: Ctx) => dom.getById<HTMLButtonElement>(ctx, dom.getDecrementTriggerId(ctx)),
|
|
17
|
+
getScrubberEl: (ctx: Ctx) => dom.getById(ctx, dom.getScrubberId(ctx)),
|
|
18
|
+
getCursorEl: (ctx: Ctx) => dom.getDoc(ctx).getElementById(dom.getCursorId(ctx)),
|
|
19
|
+
|
|
20
|
+
getPressedTriggerEl: (ctx: Ctx, hint = ctx.hint) => {
|
|
21
|
+
let btnEl: HTMLButtonElement | null = null
|
|
22
|
+
if (hint === "increment") {
|
|
23
|
+
btnEl = dom.getIncrementTriggerEl(ctx)
|
|
24
|
+
}
|
|
25
|
+
if (hint === "decrement") {
|
|
26
|
+
btnEl = dom.getDecrementTriggerEl(ctx)
|
|
27
|
+
}
|
|
28
|
+
return btnEl
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
setupVirtualCursor(ctx: Ctx) {
|
|
32
|
+
if (isSafari()) return
|
|
33
|
+
dom.createVirtualCursor(ctx)
|
|
34
|
+
return () => {
|
|
35
|
+
dom.getCursorEl(ctx)?.remove()
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
preventTextSelection(ctx: Ctx) {
|
|
40
|
+
const doc = dom.getDoc(ctx)
|
|
41
|
+
const html = doc.documentElement
|
|
42
|
+
const body = doc.body
|
|
43
|
+
|
|
44
|
+
body.style.pointerEvents = "none"
|
|
45
|
+
html.style.userSelect = "none"
|
|
46
|
+
html.style.cursor = "ew-resize"
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
body.style.pointerEvents = ""
|
|
50
|
+
html.style.userSelect = ""
|
|
51
|
+
html.style.cursor = ""
|
|
52
|
+
if (!html.style.length) {
|
|
53
|
+
html.removeAttribute("style")
|
|
54
|
+
}
|
|
55
|
+
if (!body.style.length) {
|
|
56
|
+
body.removeAttribute("style")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
getMousementValue(ctx: Ctx, event: MouseEvent) {
|
|
62
|
+
const x = roundToDevicePixel(event.movementX)
|
|
63
|
+
const y = roundToDevicePixel(event.movementY)
|
|
64
|
+
|
|
65
|
+
let hint = x > 0 ? "increment" : x < 0 ? "decrement" : null
|
|
66
|
+
|
|
67
|
+
if (ctx.isRtl && hint === "increment") hint = "decrement"
|
|
68
|
+
if (ctx.isRtl && hint === "decrement") hint = "increment"
|
|
69
|
+
|
|
70
|
+
const point = {
|
|
71
|
+
x: ctx.scrubberCursorPoint!.x + x,
|
|
72
|
+
y: ctx.scrubberCursorPoint!.y + y,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const win = dom.getWin(ctx)
|
|
76
|
+
const width = win.innerWidth
|
|
77
|
+
const half = roundToDevicePixel(7.5)
|
|
78
|
+
point.x = wrap(point.x + half, width) - half
|
|
79
|
+
|
|
80
|
+
return { hint, point }
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
createVirtualCursor(ctx: Ctx) {
|
|
84
|
+
const doc = dom.getDoc(ctx)
|
|
85
|
+
const el = doc.createElement("div")
|
|
86
|
+
el.className = "scrubber--cursor"
|
|
87
|
+
el.id = dom.getCursorId(ctx)
|
|
88
|
+
|
|
89
|
+
Object.assign(el.style, {
|
|
90
|
+
width: "15px",
|
|
91
|
+
height: "15px",
|
|
92
|
+
position: "fixed",
|
|
93
|
+
pointerEvents: "none",
|
|
94
|
+
left: "0px",
|
|
95
|
+
top: "0px",
|
|
96
|
+
zIndex: MAX_Z_INDEX,
|
|
97
|
+
transform: ctx.scrubberCursorPoint
|
|
98
|
+
? `translate3d(${ctx.scrubberCursorPoint.x}px, ${ctx.scrubberCursorPoint.y}px, 0px)`
|
|
99
|
+
: undefined,
|
|
100
|
+
willChange: "transform",
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
el.innerHTML = `
|
|
104
|
+
<svg width="46" height="15" style="left: -15.5px; position: absolute; top: 0; filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 1px 1.1px);">
|
|
105
|
+
<g transform="translate(2 3)">
|
|
106
|
+
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" style="stroke-width: 2px; stroke: white;"></path>
|
|
107
|
+
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z"></path>
|
|
108
|
+
</g>
|
|
109
|
+
</svg>`
|
|
110
|
+
|
|
111
|
+
doc.body.appendChild(el)
|
|
112
|
+
},
|
|
113
|
+
})
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { choose, createMachine, guards } from "@zag-js/core"
|
|
2
|
+
import { addDomEvent, requestPointerLock } from "@zag-js/dom-event"
|
|
3
|
+
import { isSafari, raf } from "@zag-js/dom-query"
|
|
4
|
+
import { dispatchInputValueEvent } from "@zag-js/form-utils"
|
|
5
|
+
import { observeAttributes } from "@zag-js/mutation-observer"
|
|
6
|
+
import { isAtMax, isAtMin, isWithinRange, valueOf } from "@zag-js/number-utils"
|
|
7
|
+
import { callAll, compact } from "@zag-js/utils"
|
|
8
|
+
import { dom } from "./number-input.dom"
|
|
9
|
+
import type { MachineContext, MachineState, UserDefinedContext } from "./number-input.types"
|
|
10
|
+
import { utils } from "./number-input.utils"
|
|
11
|
+
|
|
12
|
+
const { not, and } = guards
|
|
13
|
+
|
|
14
|
+
export function machine(userContext: UserDefinedContext) {
|
|
15
|
+
const ctx = compact(userContext)
|
|
16
|
+
return createMachine<MachineContext, MachineState>(
|
|
17
|
+
{
|
|
18
|
+
id: "number-input",
|
|
19
|
+
initial: "idle",
|
|
20
|
+
context: {
|
|
21
|
+
dir: "ltr",
|
|
22
|
+
focusInputOnChange: true,
|
|
23
|
+
clampValueOnBlur: true,
|
|
24
|
+
allowOverflow: false,
|
|
25
|
+
inputMode: "decimal",
|
|
26
|
+
pattern: "[0-9]*(.[0-9]+)?",
|
|
27
|
+
hint: null,
|
|
28
|
+
value: "",
|
|
29
|
+
step: 1,
|
|
30
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
31
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
32
|
+
scrubberCursorPoint: null,
|
|
33
|
+
invalid: false,
|
|
34
|
+
spinOnPress: true,
|
|
35
|
+
...ctx,
|
|
36
|
+
translations: {
|
|
37
|
+
incrementLabel: "increment value",
|
|
38
|
+
decrementLabel: "decrease value",
|
|
39
|
+
...ctx.translations,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
computed: {
|
|
44
|
+
isRtl: (ctx) => ctx.dir === "rtl",
|
|
45
|
+
valueAsNumber: (ctx) => valueOf(ctx.value),
|
|
46
|
+
isAtMin: (ctx) => isAtMin(ctx.value, ctx),
|
|
47
|
+
isAtMax: (ctx) => isAtMax(ctx.value, ctx),
|
|
48
|
+
isOutOfRange: (ctx) => !isWithinRange(ctx.value, ctx),
|
|
49
|
+
isValueEmpty: (ctx) => ctx.value === "",
|
|
50
|
+
canIncrement: (ctx) => ctx.allowOverflow || !ctx.isAtMax,
|
|
51
|
+
canDecrement: (ctx) => ctx.allowOverflow || !ctx.isAtMin,
|
|
52
|
+
valueText: (ctx) => ctx.translations.valueText?.(ctx.value),
|
|
53
|
+
formattedValue: (ctx) => ctx.format?.(ctx.value).toString() ?? ctx.value,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
watch: {
|
|
57
|
+
value: ["invokeOnChange", "dispatchChangeEvent"],
|
|
58
|
+
isOutOfRange: ["invokeOnInvalid"],
|
|
59
|
+
scrubberCursorPoint: ["setVirtualCursorPosition"],
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
entry: ["syncInputValue"],
|
|
63
|
+
|
|
64
|
+
on: {
|
|
65
|
+
SET_VALUE: [
|
|
66
|
+
{
|
|
67
|
+
guard: "clampOnBlur",
|
|
68
|
+
actions: ["setValue", "clampValue", "setHintToSet"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
actions: ["setValue", "setHintToSet"],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
CLEAR_VALUE: {
|
|
75
|
+
actions: ["clearValue"],
|
|
76
|
+
},
|
|
77
|
+
INCREMENT: {
|
|
78
|
+
actions: ["increment"],
|
|
79
|
+
},
|
|
80
|
+
DECREMENT: {
|
|
81
|
+
actions: ["decrement"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
states: {
|
|
86
|
+
idle: {
|
|
87
|
+
exit: "invokeOnFocus",
|
|
88
|
+
on: {
|
|
89
|
+
PRESS_DOWN: {
|
|
90
|
+
target: "before:spin",
|
|
91
|
+
actions: ["focusInput", "setHint"],
|
|
92
|
+
},
|
|
93
|
+
PRESS_DOWN_SCRUBBER: {
|
|
94
|
+
target: "scrubbing",
|
|
95
|
+
actions: ["focusInput", "setHint", "setCursorPoint"],
|
|
96
|
+
},
|
|
97
|
+
FOCUS: "focused",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
focused: {
|
|
102
|
+
tags: "focus",
|
|
103
|
+
entry: "focusInput",
|
|
104
|
+
activities: "attachWheelListener",
|
|
105
|
+
on: {
|
|
106
|
+
PRESS_DOWN: {
|
|
107
|
+
target: "before:spin",
|
|
108
|
+
actions: ["focusInput", "setHint"],
|
|
109
|
+
},
|
|
110
|
+
PRESS_DOWN_SCRUBBER: {
|
|
111
|
+
target: "scrubbing",
|
|
112
|
+
actions: ["focusInput", "setHint", "setCursorPoint"],
|
|
113
|
+
},
|
|
114
|
+
ARROW_UP: {
|
|
115
|
+
actions: "increment",
|
|
116
|
+
},
|
|
117
|
+
ARROW_DOWN: {
|
|
118
|
+
actions: "decrement",
|
|
119
|
+
},
|
|
120
|
+
HOME: {
|
|
121
|
+
actions: "setToMin",
|
|
122
|
+
},
|
|
123
|
+
END: {
|
|
124
|
+
actions: "setToMax",
|
|
125
|
+
},
|
|
126
|
+
CHANGE: {
|
|
127
|
+
actions: ["setValue", "setHint"],
|
|
128
|
+
},
|
|
129
|
+
BLUR: [
|
|
130
|
+
{
|
|
131
|
+
guard: "isInvalidExponential",
|
|
132
|
+
target: "idle",
|
|
133
|
+
actions: ["clearValue", "clearHint", "invokeOnBlur"],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
guard: and("clampOnBlur", not("isInRange"), not("isEmptyValue")),
|
|
137
|
+
target: "idle",
|
|
138
|
+
actions: ["clampValue", "clearHint", "invokeOnBlur"],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
target: "idle",
|
|
142
|
+
actions: ["roundValue", "invokeOnBlur"],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
"before:spin": {
|
|
149
|
+
tags: "focus",
|
|
150
|
+
activities: "trackButtonDisabled",
|
|
151
|
+
entry: choose([
|
|
152
|
+
{ guard: "isIncrementHint", actions: "increment" },
|
|
153
|
+
{ guard: "isDecrementHint", actions: "decrement" },
|
|
154
|
+
]),
|
|
155
|
+
after: {
|
|
156
|
+
CHANGE_DELAY: {
|
|
157
|
+
target: "spinning",
|
|
158
|
+
guard: and("isInRange", "spinOnPress"),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
on: {
|
|
162
|
+
PRESS_UP: {
|
|
163
|
+
target: "focused",
|
|
164
|
+
actions: "clearHint",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
spinning: {
|
|
170
|
+
tags: "focus",
|
|
171
|
+
activities: "trackButtonDisabled",
|
|
172
|
+
every: [
|
|
173
|
+
{
|
|
174
|
+
delay: "CHANGE_INTERVAL",
|
|
175
|
+
guard: and(not("isAtMin"), "isIncrementHint"),
|
|
176
|
+
actions: "increment",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
delay: "CHANGE_INTERVAL",
|
|
180
|
+
guard: and(not("isAtMax"), "isDecrementHint"),
|
|
181
|
+
actions: "decrement",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
on: {
|
|
185
|
+
PRESS_UP: {
|
|
186
|
+
target: "focused",
|
|
187
|
+
actions: "clearHint",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
scrubbing: {
|
|
193
|
+
tags: "focus",
|
|
194
|
+
exit: "clearCursorPoint",
|
|
195
|
+
activities: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"],
|
|
196
|
+
on: {
|
|
197
|
+
POINTER_UP_SCRUBBER: "focused",
|
|
198
|
+
POINTER_MOVE_SCRUBBER: [
|
|
199
|
+
{
|
|
200
|
+
guard: "isIncrementHint",
|
|
201
|
+
actions: ["increment", "setCursorPoint"],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
guard: "isDecrementHint",
|
|
205
|
+
actions: ["decrement", "setCursorPoint"],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
delays: {
|
|
214
|
+
CHANGE_INTERVAL: 50,
|
|
215
|
+
CHANGE_DELAY: 300,
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
guards: {
|
|
219
|
+
clampOnBlur: (ctx) => !!ctx.clampValueOnBlur,
|
|
220
|
+
isAtMin: (ctx) => ctx.isAtMin,
|
|
221
|
+
spinOnPress: (ctx) => !!ctx.spinOnPress,
|
|
222
|
+
isAtMax: (ctx) => ctx.isAtMax,
|
|
223
|
+
isInRange: (ctx) => !ctx.isOutOfRange,
|
|
224
|
+
isDecrementHint: (ctx, evt) => (evt.hint ?? ctx.hint) === "decrement",
|
|
225
|
+
isEmptyValue: (ctx) => ctx.isValueEmpty,
|
|
226
|
+
isIncrementHint: (ctx, evt) => (evt.hint ?? ctx.hint) === "increment",
|
|
227
|
+
isInvalidExponential: (ctx) => ctx.value.toString().startsWith("e"),
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
activities: {
|
|
231
|
+
setupVirtualCursor(ctx) {
|
|
232
|
+
return dom.setupVirtualCursor(ctx)
|
|
233
|
+
},
|
|
234
|
+
preventTextSelection(ctx) {
|
|
235
|
+
return dom.preventTextSelection(ctx)
|
|
236
|
+
},
|
|
237
|
+
trackButtonDisabled(ctx, _evt, { send }) {
|
|
238
|
+
const btn = dom.getPressedTriggerEl(ctx, ctx.hint)
|
|
239
|
+
return observeAttributes(btn, ["disabled"], () => {
|
|
240
|
+
send("PRESS_UP")
|
|
241
|
+
})
|
|
242
|
+
},
|
|
243
|
+
attachWheelListener(ctx, _evt, { send }) {
|
|
244
|
+
const input = dom.getInputEl(ctx)
|
|
245
|
+
if (!input) return
|
|
246
|
+
|
|
247
|
+
function onWheel(event: WheelEvent) {
|
|
248
|
+
const isInputFocused = dom.getDoc(ctx).activeElement === input
|
|
249
|
+
if (!ctx.allowMouseWheel || !isInputFocused) return
|
|
250
|
+
event.preventDefault()
|
|
251
|
+
|
|
252
|
+
const dir = Math.sign(event.deltaY) * -1
|
|
253
|
+
if (dir === 1) {
|
|
254
|
+
send("INCREMENT")
|
|
255
|
+
} else if (dir === -1) {
|
|
256
|
+
send("DECREMENT")
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return addDomEvent(input, "wheel", onWheel, { passive: false })
|
|
260
|
+
},
|
|
261
|
+
activatePointerLock(ctx) {
|
|
262
|
+
if (isSafari()) return
|
|
263
|
+
return requestPointerLock(dom.getDoc(ctx))
|
|
264
|
+
},
|
|
265
|
+
trackMousemove(ctx, _evt, { send }) {
|
|
266
|
+
const doc = dom.getDoc(ctx)
|
|
267
|
+
|
|
268
|
+
function onMousemove(event: MouseEvent) {
|
|
269
|
+
if (!ctx.scrubberCursorPoint) return
|
|
270
|
+
const value = dom.getMousementValue(ctx, event)
|
|
271
|
+
if (!value.hint) return
|
|
272
|
+
send({
|
|
273
|
+
type: "POINTER_MOVE_SCRUBBER",
|
|
274
|
+
hint: value.hint,
|
|
275
|
+
point: value.point,
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function onMouseup() {
|
|
280
|
+
send("POINTER_UP_SCRUBBER")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return callAll(
|
|
284
|
+
addDomEvent(doc, "mousemove", onMousemove, false),
|
|
285
|
+
addDomEvent(doc, "mouseup", onMouseup, false),
|
|
286
|
+
)
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
actions: {
|
|
291
|
+
focusInput(ctx) {
|
|
292
|
+
if (!ctx.focusInputOnChange) return
|
|
293
|
+
const input = dom.getInputEl(ctx)
|
|
294
|
+
raf(() => input?.focus())
|
|
295
|
+
},
|
|
296
|
+
increment(ctx, evt) {
|
|
297
|
+
ctx.value = utils.increment(ctx, evt.step)
|
|
298
|
+
},
|
|
299
|
+
decrement(ctx, evt) {
|
|
300
|
+
ctx.value = utils.decrement(ctx, evt.step)
|
|
301
|
+
},
|
|
302
|
+
clampValue(ctx) {
|
|
303
|
+
ctx.value = utils.clamp(ctx)
|
|
304
|
+
},
|
|
305
|
+
roundValue(ctx) {
|
|
306
|
+
if (ctx.value !== "") {
|
|
307
|
+
ctx.value = utils.round(ctx)
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
setValue(ctx, evt) {
|
|
311
|
+
const value = evt.target?.value ?? evt.value
|
|
312
|
+
ctx.value = utils.sanitize(ctx, utils.parse(ctx, value.toString()))
|
|
313
|
+
},
|
|
314
|
+
clearValue(ctx) {
|
|
315
|
+
ctx.value = ""
|
|
316
|
+
},
|
|
317
|
+
setToMax(ctx) {
|
|
318
|
+
ctx.value = ctx.max.toString()
|
|
319
|
+
},
|
|
320
|
+
setToMin(ctx) {
|
|
321
|
+
ctx.value = ctx.min.toString()
|
|
322
|
+
},
|
|
323
|
+
setHint(ctx, evt) {
|
|
324
|
+
ctx.hint = evt.hint
|
|
325
|
+
},
|
|
326
|
+
clearHint(ctx) {
|
|
327
|
+
ctx.hint = null
|
|
328
|
+
},
|
|
329
|
+
setHintToSet(ctx) {
|
|
330
|
+
ctx.hint = "set"
|
|
331
|
+
},
|
|
332
|
+
invokeOnChange(ctx) {
|
|
333
|
+
ctx.onChange?.({
|
|
334
|
+
value: ctx.value,
|
|
335
|
+
valueAsNumber: ctx.valueAsNumber,
|
|
336
|
+
})
|
|
337
|
+
},
|
|
338
|
+
invokeOnFocus(ctx, evt) {
|
|
339
|
+
let srcElement: HTMLElement | null = null
|
|
340
|
+
|
|
341
|
+
if (evt.type === "PRESS_DOWN") {
|
|
342
|
+
srcElement = dom.getPressedTriggerEl(ctx, evt.hint)
|
|
343
|
+
} else if (evt.type === "FOCUS") {
|
|
344
|
+
srcElement = dom.getInputEl(ctx)
|
|
345
|
+
} else if (evt.type === "PRESS_DOWN_SCRUBBER") {
|
|
346
|
+
srcElement = dom.getScrubberEl(ctx)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ctx.onFocus?.({
|
|
350
|
+
value: ctx.value,
|
|
351
|
+
valueAsNumber: ctx.valueAsNumber,
|
|
352
|
+
srcElement,
|
|
353
|
+
})
|
|
354
|
+
},
|
|
355
|
+
invokeOnBlur(ctx) {
|
|
356
|
+
ctx.onBlur?.({
|
|
357
|
+
value: ctx.value,
|
|
358
|
+
valueAsNumber: ctx.valueAsNumber,
|
|
359
|
+
})
|
|
360
|
+
},
|
|
361
|
+
invokeOnInvalid(ctx) {
|
|
362
|
+
if (!ctx.isOutOfRange) return
|
|
363
|
+
const reason = ctx.valueAsNumber > ctx.max ? "rangeOverflow" : "rangeUnderflow"
|
|
364
|
+
ctx.onInvalid?.({
|
|
365
|
+
reason,
|
|
366
|
+
value: ctx.formattedValue,
|
|
367
|
+
valueAsNumber: ctx.valueAsNumber,
|
|
368
|
+
})
|
|
369
|
+
},
|
|
370
|
+
// sync input value, in event it was set from form libraries via `ref`, `bind:this`, etc.
|
|
371
|
+
syncInputValue(ctx) {
|
|
372
|
+
const input = dom.getInputEl(ctx)
|
|
373
|
+
if (!input || input.value == ctx.value) return
|
|
374
|
+
const value = utils.parse(ctx, input.value)
|
|
375
|
+
ctx.value = utils.sanitize(ctx, value)
|
|
376
|
+
},
|
|
377
|
+
setCursorPoint(ctx, evt) {
|
|
378
|
+
ctx.scrubberCursorPoint = evt.point
|
|
379
|
+
},
|
|
380
|
+
clearCursorPoint(ctx) {
|
|
381
|
+
ctx.scrubberCursorPoint = null
|
|
382
|
+
},
|
|
383
|
+
setVirtualCursorPosition(ctx) {
|
|
384
|
+
const cursor = dom.getCursorEl(ctx)
|
|
385
|
+
if (!cursor || !ctx.scrubberCursorPoint) return
|
|
386
|
+
const { x, y } = ctx.scrubberCursorPoint
|
|
387
|
+
cursor.style.transform = `translate3d(${x}px, ${y}px, 0px)`
|
|
388
|
+
},
|
|
389
|
+
dispatchChangeEvent(ctx) {
|
|
390
|
+
const inputEl = dom.getInputEl(ctx)
|
|
391
|
+
dispatchInputValueEvent(inputEl, { value: ctx.formattedValue })
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { StateMachine as S } from "@zag-js/core"
|
|
2
|
+
import type { CommonProperties, Context, DirectionProperty, RequiredBy } from "@zag-js/types"
|
|
3
|
+
|
|
4
|
+
type ValidityState = "rangeUnderflow" | "rangeOverflow"
|
|
5
|
+
|
|
6
|
+
type ElementIds = Partial<{
|
|
7
|
+
root: string
|
|
8
|
+
label: string
|
|
9
|
+
input: string
|
|
10
|
+
incrementTrigger: string
|
|
11
|
+
decrementTrigger: string
|
|
12
|
+
scrubber: string
|
|
13
|
+
}>
|
|
14
|
+
|
|
15
|
+
type IntlTranslations = {
|
|
16
|
+
/**
|
|
17
|
+
* Function that returns the human-readable value.
|
|
18
|
+
* It is used to set the `aria-valuetext` property of the input
|
|
19
|
+
*/
|
|
20
|
+
valueText?: (value: string) => string
|
|
21
|
+
/**
|
|
22
|
+
* The label foe the increment button
|
|
23
|
+
*/
|
|
24
|
+
incrementLabel: string
|
|
25
|
+
/**
|
|
26
|
+
* The label for the decrement button
|
|
27
|
+
*/
|
|
28
|
+
decrementLabel: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Value = {
|
|
32
|
+
value: string
|
|
33
|
+
valueAsNumber: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type PublicContext = DirectionProperty &
|
|
37
|
+
CommonProperties & {
|
|
38
|
+
/**
|
|
39
|
+
* The ids of the elements in the number input. Useful for composition.
|
|
40
|
+
*/
|
|
41
|
+
ids?: ElementIds
|
|
42
|
+
/**
|
|
43
|
+
* The name attribute of the number input. Useful for form submission.
|
|
44
|
+
*/
|
|
45
|
+
name?: string
|
|
46
|
+
/**
|
|
47
|
+
* The associate form of the input element.
|
|
48
|
+
*/
|
|
49
|
+
form?: string
|
|
50
|
+
/**
|
|
51
|
+
* Whether the number input is disabled.
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Whether the number input is readonly
|
|
56
|
+
*/
|
|
57
|
+
readOnly?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* Whether the number input value is invalid.
|
|
60
|
+
*/
|
|
61
|
+
invalid?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* The pattern used to check the <input> element's value against
|
|
64
|
+
*
|
|
65
|
+
* @default
|
|
66
|
+
* "[0-9]*(.[0-9]+)?"
|
|
67
|
+
*/
|
|
68
|
+
pattern: string
|
|
69
|
+
/**
|
|
70
|
+
* The value of the input
|
|
71
|
+
*/
|
|
72
|
+
value: string
|
|
73
|
+
/**
|
|
74
|
+
* The minimum value of the number input
|
|
75
|
+
*/
|
|
76
|
+
min: number
|
|
77
|
+
/**
|
|
78
|
+
* The maximum value of the number input
|
|
79
|
+
*/
|
|
80
|
+
max: number
|
|
81
|
+
/**
|
|
82
|
+
* The amount to increment or decrement the value by
|
|
83
|
+
*/
|
|
84
|
+
step: number
|
|
85
|
+
/**
|
|
86
|
+
* Whether to allow mouse wheel to change the value
|
|
87
|
+
*/
|
|
88
|
+
allowMouseWheel?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* Whether to allow the value overflow the min/max range
|
|
91
|
+
* @default true
|
|
92
|
+
*/
|
|
93
|
+
allowOverflow: boolean
|
|
94
|
+
/**
|
|
95
|
+
* Whether the pressed key should be allowed in the input.
|
|
96
|
+
* The default behavior is to allow DOM floating point characters defined by /^[Ee0-9+\-.]$/
|
|
97
|
+
*/
|
|
98
|
+
validateCharacter?: (char: string) => boolean
|
|
99
|
+
/**
|
|
100
|
+
* Whether to clamp the value when the input loses focus (blur)
|
|
101
|
+
* @default true
|
|
102
|
+
*/
|
|
103
|
+
clampValueOnBlur: boolean
|
|
104
|
+
/**
|
|
105
|
+
* Whether to focus input when the value changes
|
|
106
|
+
* @default true
|
|
107
|
+
*/
|
|
108
|
+
focusInputOnChange: boolean
|
|
109
|
+
/**
|
|
110
|
+
* Specifies the localized strings that identifies the accessibility elements and their states
|
|
111
|
+
*/
|
|
112
|
+
translations: IntlTranslations
|
|
113
|
+
/**
|
|
114
|
+
* If using a custom display format, this converts the custom format to a format `parseFloat` understands.
|
|
115
|
+
*/
|
|
116
|
+
parse?: (value: string) => string
|
|
117
|
+
/**
|
|
118
|
+
* If using a custom display format, this converts the default format to the custom format.
|
|
119
|
+
*/
|
|
120
|
+
format?: (value: string) => string | number
|
|
121
|
+
/**
|
|
122
|
+
* Hints at the type of data that might be entered by the user. It also determines
|
|
123
|
+
* the type of keyboard shown to the user on mobile devices
|
|
124
|
+
* @default "decimal"
|
|
125
|
+
*/
|
|
126
|
+
inputMode: "text" | "tel" | "numeric" | "decimal"
|
|
127
|
+
/**
|
|
128
|
+
* Function invoked when the value changes
|
|
129
|
+
*/
|
|
130
|
+
onChange?: (details: Value) => void
|
|
131
|
+
/**
|
|
132
|
+
* Function invoked when the value overflows or underflows the min/max range
|
|
133
|
+
*/
|
|
134
|
+
onInvalid?: (details: Value & { reason: ValidityState }) => void
|
|
135
|
+
/**
|
|
136
|
+
* Function invoked when the number input is focused
|
|
137
|
+
*/
|
|
138
|
+
onFocus?: (details: Value & { srcElement: HTMLElement | null }) => void
|
|
139
|
+
/**
|
|
140
|
+
* The value of the input when it is blurred
|
|
141
|
+
*/
|
|
142
|
+
onBlur?: (details: Value) => void
|
|
143
|
+
/**
|
|
144
|
+
* The minimum number of fraction digits to use. Possible values are from 0 to 20
|
|
145
|
+
*/
|
|
146
|
+
minFractionDigits?: number
|
|
147
|
+
/**
|
|
148
|
+
* The maximum number of fraction digits to use. Possible values are from 0 to 20;
|
|
149
|
+
*/
|
|
150
|
+
maxFractionDigits?: number
|
|
151
|
+
/**
|
|
152
|
+
* Whether to spin the value when the increment/decrement button is pressed
|
|
153
|
+
*/
|
|
154
|
+
spinOnPress?: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type UserDefinedContext = RequiredBy<PublicContext, "id">
|
|
158
|
+
|
|
159
|
+
type ComputedContext = Readonly<{
|
|
160
|
+
/**
|
|
161
|
+
* @computed
|
|
162
|
+
* The value of the input as a number
|
|
163
|
+
*/
|
|
164
|
+
valueAsNumber: number
|
|
165
|
+
/**
|
|
166
|
+
* @computed
|
|
167
|
+
* Whether the value is at the min
|
|
168
|
+
*/
|
|
169
|
+
isAtMin: boolean
|
|
170
|
+
/**
|
|
171
|
+
* @computed
|
|
172
|
+
* Whether the value is at the max
|
|
173
|
+
*/
|
|
174
|
+
isAtMax: boolean
|
|
175
|
+
/**
|
|
176
|
+
* @computed
|
|
177
|
+
* Whether the value is out of the min/max range
|
|
178
|
+
*/
|
|
179
|
+
isOutOfRange: boolean
|
|
180
|
+
/**
|
|
181
|
+
* @computed
|
|
182
|
+
* Whether the value is empty
|
|
183
|
+
*/
|
|
184
|
+
isValueEmpty: boolean
|
|
185
|
+
/**
|
|
186
|
+
* @computed
|
|
187
|
+
* Whether the increment button is enabled
|
|
188
|
+
*/
|
|
189
|
+
canIncrement: boolean
|
|
190
|
+
/**
|
|
191
|
+
* @computed
|
|
192
|
+
* Whether the decrement button is enabled
|
|
193
|
+
*/
|
|
194
|
+
canDecrement: boolean
|
|
195
|
+
/**
|
|
196
|
+
* @computed
|
|
197
|
+
* The `aria-valuetext` attribute of the input
|
|
198
|
+
*/
|
|
199
|
+
valueText: string | undefined
|
|
200
|
+
/**
|
|
201
|
+
* @computed
|
|
202
|
+
* The formatted value of the input
|
|
203
|
+
*/
|
|
204
|
+
formattedValue: string
|
|
205
|
+
/**
|
|
206
|
+
* @computed
|
|
207
|
+
* Whether the writing direction is RTL
|
|
208
|
+
*/
|
|
209
|
+
isRtl: boolean
|
|
210
|
+
}>
|
|
211
|
+
|
|
212
|
+
type PrivateContext = Context<{
|
|
213
|
+
/**
|
|
214
|
+
* @internal
|
|
215
|
+
* The hint that determines if we're incrementing or decrementing
|
|
216
|
+
*/
|
|
217
|
+
hint: "increment" | "decrement" | "set" | null
|
|
218
|
+
/**
|
|
219
|
+
* @internal
|
|
220
|
+
* The scrubber cursor position
|
|
221
|
+
*/
|
|
222
|
+
scrubberCursorPoint: { x: number; y: number } | null
|
|
223
|
+
}>
|
|
224
|
+
|
|
225
|
+
export type MachineContext = PublicContext & PrivateContext & ComputedContext
|
|
226
|
+
|
|
227
|
+
export type MachineState = {
|
|
228
|
+
value: "idle" | "focused" | "spinning" | "before:spin" | "scrubbing"
|
|
229
|
+
tags: "focus"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type State = S.State<MachineContext, MachineState>
|
|
233
|
+
|
|
234
|
+
export type Send = S.Send<S.AnyEventObject>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isModifiedEvent } from "@zag-js/dom-event"
|
|
2
|
+
import { clamp, decrement, formatDecimal, increment } from "@zag-js/number-utils"
|
|
3
|
+
import type { JSX } from "@zag-js/types"
|
|
4
|
+
import type { MachineContext as Ctx } from "./number-input.types"
|
|
5
|
+
|
|
6
|
+
export const utils = {
|
|
7
|
+
isValidNumericEvent: (ctx: Ctx, event: JSX.KeyboardEvent) => {
|
|
8
|
+
if (event.key == null) return true
|
|
9
|
+
const isModifier = isModifiedEvent(event)
|
|
10
|
+
const isSingleKey = event.key.length === 1
|
|
11
|
+
if (isModifier || !isSingleKey) return true
|
|
12
|
+
return ctx.validateCharacter?.(event.key) ?? utils.isFloatingPoint(event.key)
|
|
13
|
+
},
|
|
14
|
+
isFloatingPoint: (v: string) => /^[0-9+\-.]$/.test(v),
|
|
15
|
+
sanitize: (ctx: Ctx, value: string) => {
|
|
16
|
+
return value
|
|
17
|
+
.split("")
|
|
18
|
+
.filter(ctx.validateCharacter ?? utils.isFloatingPoint)
|
|
19
|
+
.join("")
|
|
20
|
+
},
|
|
21
|
+
increment: (ctx: Ctx, step?: number) => {
|
|
22
|
+
const value = increment(ctx.value, step ?? ctx.step)
|
|
23
|
+
return formatDecimal(clamp(value, ctx), ctx)
|
|
24
|
+
},
|
|
25
|
+
decrement: (ctx: Ctx, step?: number) => {
|
|
26
|
+
const value = decrement(ctx.value, step ?? ctx.step)
|
|
27
|
+
return formatDecimal(clamp(value, ctx), ctx)
|
|
28
|
+
},
|
|
29
|
+
clamp: (ctx: Ctx) => {
|
|
30
|
+
return formatDecimal(clamp(ctx.value, ctx), ctx)
|
|
31
|
+
},
|
|
32
|
+
parse: (ctx: Ctx, value: string) => {
|
|
33
|
+
return ctx.parse?.(value) ?? value
|
|
34
|
+
},
|
|
35
|
+
format: (ctx: Ctx, value: string | number) => {
|
|
36
|
+
const _val = value.toString()
|
|
37
|
+
return ctx.format?.(_val) ?? _val
|
|
38
|
+
},
|
|
39
|
+
round: (ctx: Ctx) => {
|
|
40
|
+
return formatDecimal(ctx.value, ctx)
|
|
41
|
+
},
|
|
42
|
+
}
|