@zag-js/number-input 0.69.0 → 0.71.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,113 +0,0 @@
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
- })
@@ -1,445 +0,0 @@
1
- import { choose, createMachine, guards } from "@zag-js/core"
2
- import { addDomEvent, requestPointerLock } from "@zag-js/dom-event"
3
- import { isSafari, observeAttributes, raf } from "@zag-js/dom-query"
4
- import { trackFormControl } from "@zag-js/form-utils"
5
- import { clamp, decrement, increment, isAtMax, isAtMin, isWithinRange } from "@zag-js/number-utils"
6
- import { callAll, compact, isEqual } from "@zag-js/utils"
7
- import { recordCursor, restoreCursor } from "./cursor"
8
- import { dom } from "./number-input.dom"
9
- import type { MachineContext, MachineState, UserDefinedContext } from "./number-input.types"
10
- import { createFormatter, createParser, formatValue, parseValue } 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
- locale: "en-US",
23
- focusInputOnChange: true,
24
- clampValueOnBlur: true,
25
- allowOverflow: false,
26
- inputMode: "decimal",
27
- pattern: "[0-9]*(.[0-9]+)?",
28
- value: "",
29
- step: 1,
30
- min: Number.MIN_SAFE_INTEGER,
31
- max: Number.MAX_SAFE_INTEGER,
32
- invalid: false,
33
- spinOnPress: true,
34
- disabled: false,
35
- readOnly: false,
36
- ...ctx,
37
- hint: null,
38
- scrubberCursorPoint: null,
39
- fieldsetDisabled: false,
40
- formatter: createFormatter(ctx.locale || "en-US", ctx.formatOptions),
41
- parser: createParser(ctx.locale || "en-US", ctx.formatOptions),
42
- translations: {
43
- incrementLabel: "increment value",
44
- decrementLabel: "decrease value",
45
- ...ctx.translations,
46
- },
47
- },
48
-
49
- computed: {
50
- isRtl: (ctx) => ctx.dir === "rtl",
51
- valueAsNumber: (ctx) => parseValue(ctx, ctx.value),
52
- formattedValue: (ctx) => formatValue(ctx, ctx.valueAsNumber),
53
- isAtMin: (ctx) => isAtMin(ctx.valueAsNumber, ctx),
54
- isAtMax: (ctx) => isAtMax(ctx.valueAsNumber, ctx),
55
- isOutOfRange: (ctx) => !isWithinRange(ctx.valueAsNumber, ctx),
56
- isValueEmpty: (ctx) => ctx.value === "",
57
- isDisabled: (ctx) => !!ctx.disabled || ctx.fieldsetDisabled,
58
- canIncrement: (ctx) => ctx.allowOverflow || !ctx.isAtMax,
59
- canDecrement: (ctx) => ctx.allowOverflow || !ctx.isAtMin,
60
- valueText: (ctx) => ctx.translations.valueText?.(ctx.value),
61
- },
62
-
63
- watch: {
64
- formatOptions: ["setFormatterAndParser", "syncInputElement"],
65
- locale: ["setFormatterAndParser", "syncInputElement"],
66
- value: ["syncInputElement"],
67
- isOutOfRange: ["invokeOnInvalid"],
68
- scrubberCursorPoint: ["setVirtualCursorPosition"],
69
- },
70
-
71
- activities: ["trackFormControl"],
72
-
73
- on: {
74
- "VALUE.SET": {
75
- actions: ["setRawValue", "setHintToSet"],
76
- },
77
- "VALUE.CLEAR": {
78
- actions: ["clearValue"],
79
- },
80
- "VALUE.INCREMENT": {
81
- actions: ["increment"],
82
- },
83
- "VALUE.DECREMENT": {
84
- actions: ["decrement"],
85
- },
86
- },
87
-
88
- states: {
89
- idle: {
90
- on: {
91
- "TRIGGER.PRESS_DOWN": [
92
- { guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] },
93
- {
94
- target: "before:spin",
95
- actions: ["focusInput", "invokeOnFocus", "setHint"],
96
- },
97
- ],
98
- "SCRUBBER.PRESS_DOWN": {
99
- target: "scrubbing",
100
- actions: ["focusInput", "invokeOnFocus", "setHint", "setCursorPoint"],
101
- },
102
- "INPUT.FOCUS": {
103
- target: "focused",
104
- actions: ["focusInput", "invokeOnFocus"],
105
- },
106
- },
107
- },
108
-
109
- focused: {
110
- tags: "focus",
111
- activities: "attachWheelListener",
112
- on: {
113
- "TRIGGER.PRESS_DOWN": [
114
- { guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] },
115
- { target: "before:spin", actions: ["focusInput", "setHint"] },
116
- ],
117
- "SCRUBBER.PRESS_DOWN": {
118
- target: "scrubbing",
119
- actions: ["focusInput", "setHint", "setCursorPoint"],
120
- },
121
- "INPUT.ARROW_UP": {
122
- actions: "increment",
123
- },
124
- "INPUT.ARROW_DOWN": {
125
- actions: "decrement",
126
- },
127
- "INPUT.HOME": {
128
- actions: "decrementToMin",
129
- },
130
- "INPUT.END": {
131
- actions: "incrementToMax",
132
- },
133
- "INPUT.CHANGE": {
134
- actions: ["setValue", "setHint"],
135
- },
136
- "INPUT.BLUR": [
137
- {
138
- guard: and("clampValueOnBlur", not("isInRange")),
139
- target: "idle",
140
- actions: ["setClampedValue", "clearHint", "invokeOnBlur"],
141
- },
142
- {
143
- target: "idle",
144
- actions: ["setFormattedValue", "clearHint", "invokeOnBlur"],
145
- },
146
- ],
147
- "INPUT.ENTER": {
148
- actions: ["setFormattedValue", "clearHint", "invokeOnBlur"],
149
- },
150
- },
151
- },
152
-
153
- "before:spin": {
154
- tags: "focus",
155
- activities: "trackButtonDisabled",
156
- entry: choose([
157
- { guard: "isIncrementHint", actions: "increment" },
158
- { guard: "isDecrementHint", actions: "decrement" },
159
- ]),
160
- after: {
161
- CHANGE_DELAY: {
162
- target: "spinning",
163
- guard: and("isInRange", "spinOnPress"),
164
- },
165
- },
166
- on: {
167
- "TRIGGER.PRESS_UP": [
168
- { guard: "isTouchPointer", target: "focused", actions: "clearHint" },
169
- { target: "focused", actions: ["focusInput", "clearHint"] },
170
- ],
171
- },
172
- },
173
-
174
- spinning: {
175
- tags: "focus",
176
- activities: "trackButtonDisabled",
177
- every: [
178
- {
179
- delay: "CHANGE_INTERVAL",
180
- guard: and(not("isAtMin"), "isIncrementHint"),
181
- actions: "increment",
182
- },
183
- {
184
- delay: "CHANGE_INTERVAL",
185
- guard: and(not("isAtMax"), "isDecrementHint"),
186
- actions: "decrement",
187
- },
188
- ],
189
- on: {
190
- "TRIGGER.PRESS_UP": {
191
- target: "focused",
192
- actions: ["focusInput", "clearHint"],
193
- },
194
- },
195
- },
196
-
197
- scrubbing: {
198
- tags: "focus",
199
- activities: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"],
200
- on: {
201
- "SCRUBBER.POINTER_UP": {
202
- target: "focused",
203
- actions: ["focusInput", "clearCursorPoint"],
204
- },
205
- "SCRUBBER.POINTER_MOVE": [
206
- {
207
- guard: "isIncrementHint",
208
- actions: ["increment", "setCursorPoint"],
209
- },
210
- {
211
- guard: "isDecrementHint",
212
- actions: ["decrement", "setCursorPoint"],
213
- },
214
- ],
215
- },
216
- },
217
- },
218
- },
219
- {
220
- delays: {
221
- CHANGE_INTERVAL: 50,
222
- CHANGE_DELAY: 300,
223
- },
224
-
225
- guards: {
226
- clampValueOnBlur: (ctx) => ctx.clampValueOnBlur,
227
- isAtMin: (ctx) => ctx.isAtMin,
228
- spinOnPress: (ctx) => !!ctx.spinOnPress,
229
- isAtMax: (ctx) => ctx.isAtMax,
230
- isInRange: (ctx) => !ctx.isOutOfRange,
231
- isDecrementHint: (ctx, evt) => (evt.hint ?? ctx.hint) === "decrement",
232
- isIncrementHint: (ctx, evt) => (evt.hint ?? ctx.hint) === "increment",
233
- isTouchPointer: (_ctx, evt) => evt.pointerType === "touch",
234
- },
235
-
236
- activities: {
237
- trackFormControl(ctx, _evt, { initialContext }) {
238
- const inputEl = dom.getInputEl(ctx)
239
- return trackFormControl(inputEl, {
240
- onFieldsetDisabledChange(disabled) {
241
- ctx.fieldsetDisabled = disabled
242
- },
243
- onFormReset() {
244
- set.value(ctx, initialContext.value)
245
- },
246
- })
247
- },
248
- setupVirtualCursor(ctx) {
249
- return dom.setupVirtualCursor(ctx)
250
- },
251
- preventTextSelection(ctx) {
252
- return dom.preventTextSelection(ctx)
253
- },
254
- trackButtonDisabled(ctx, _evt, { send }) {
255
- const btn = dom.getPressedTriggerEl(ctx, ctx.hint)
256
- return observeAttributes(btn, {
257
- attributes: ["disabled"],
258
- callback() {
259
- send({ type: "TRIGGER.PRESS_UP", src: "attr" })
260
- },
261
- })
262
- },
263
- attachWheelListener(ctx, _evt, { send }) {
264
- const inputEl = dom.getInputEl(ctx)
265
- if (!inputEl || !dom.isActiveElement(ctx, inputEl) || !ctx.allowMouseWheel) return
266
-
267
- function onWheel(event: WheelEvent) {
268
- event.preventDefault()
269
- const dir = Math.sign(event.deltaY) * -1
270
- if (dir === 1) {
271
- send("VALUE.INCREMENT")
272
- } else if (dir === -1) {
273
- send("VALUE.DECREMENT")
274
- }
275
- }
276
-
277
- return addDomEvent(inputEl, "wheel", onWheel, { passive: false })
278
- },
279
- activatePointerLock(ctx) {
280
- if (isSafari()) return
281
- return requestPointerLock(dom.getDoc(ctx))
282
- },
283
- trackMousemove(ctx, _evt, { send }) {
284
- const doc = dom.getDoc(ctx)
285
-
286
- function onMousemove(event: MouseEvent) {
287
- if (!ctx.scrubberCursorPoint) return
288
- const value = dom.getMousementValue(ctx, event)
289
- if (!value.hint) return
290
- send({
291
- type: "SCRUBBER.POINTER_MOVE",
292
- hint: value.hint,
293
- point: value.point,
294
- })
295
- }
296
-
297
- function onMouseup() {
298
- send("SCRUBBER.POINTER_UP")
299
- }
300
-
301
- return callAll(
302
- addDomEvent(doc, "mousemove", onMousemove, false),
303
- addDomEvent(doc, "mouseup", onMouseup, false),
304
- )
305
- },
306
- },
307
-
308
- actions: {
309
- focusInput(ctx) {
310
- if (!ctx.focusInputOnChange) return
311
- const inputEl = dom.getInputEl(ctx)
312
- if (dom.isActiveElement(ctx, inputEl)) return
313
- raf(() => inputEl?.focus({ preventScroll: true }))
314
- },
315
- increment(ctx, evt) {
316
- const nextValue = increment(ctx.valueAsNumber, evt.step ?? ctx.step)
317
- const value = formatValue(ctx, clamp(nextValue, ctx))
318
- set.value(ctx, value)
319
- },
320
- decrement(ctx, evt) {
321
- const nextValue = decrement(ctx.valueAsNumber, evt.step ?? ctx.step)
322
- const value = formatValue(ctx, clamp(nextValue, ctx))
323
- set.value(ctx, value)
324
- },
325
- setClampedValue(ctx) {
326
- const nextValue = clamp(ctx.valueAsNumber, ctx)
327
- set.value(ctx, formatValue(ctx, nextValue))
328
- },
329
- setRawValue(ctx, evt) {
330
- const parsedValue = parseValue(ctx, evt.value)
331
- const value = formatValue(ctx, clamp(parsedValue, ctx))
332
- set.value(ctx, value)
333
- },
334
- setValue(ctx, evt) {
335
- const value = evt.target?.value ?? evt.value
336
- set.value(ctx, value)
337
- },
338
- clearValue(ctx) {
339
- set.value(ctx, "")
340
- },
341
- incrementToMax(ctx) {
342
- const value = formatValue(ctx, ctx.max)
343
- set.value(ctx, value)
344
- },
345
- decrementToMin(ctx) {
346
- const value = formatValue(ctx, ctx.min)
347
- set.value(ctx, value)
348
- },
349
- setHint(ctx, evt) {
350
- ctx.hint = evt.hint
351
- },
352
- clearHint(ctx) {
353
- ctx.hint = null
354
- },
355
- setHintToSet(ctx) {
356
- ctx.hint = "set"
357
- },
358
- invokeOnFocus(ctx) {
359
- ctx.onFocusChange?.({
360
- focused: true,
361
- value: ctx.formattedValue,
362
- valueAsNumber: ctx.valueAsNumber,
363
- })
364
- },
365
- invokeOnBlur(ctx) {
366
- ctx.onFocusChange?.({
367
- focused: false,
368
- value: ctx.formattedValue,
369
- valueAsNumber: ctx.valueAsNumber,
370
- })
371
- },
372
- invokeOnInvalid(ctx) {
373
- if (!ctx.isOutOfRange) return
374
- const reason = ctx.valueAsNumber > ctx.max ? "rangeOverflow" : "rangeUnderflow"
375
- ctx.onValueInvalid?.({
376
- reason,
377
- value: ctx.formattedValue,
378
- valueAsNumber: ctx.valueAsNumber,
379
- })
380
- },
381
- syncInputElement(ctx, evt) {
382
- const value = evt.type.endsWith("CHANGE") ? ctx.value : ctx.formattedValue
383
- sync.input(ctx, value)
384
- },
385
- setFormattedValue(ctx) {
386
- set.value(ctx, ctx.formattedValue)
387
- },
388
- setCursorPoint(ctx, evt) {
389
- ctx.scrubberCursorPoint = evt.point
390
- },
391
- clearCursorPoint(ctx) {
392
- ctx.scrubberCursorPoint = null
393
- },
394
- setVirtualCursorPosition(ctx) {
395
- const cursorEl = dom.getCursorEl(ctx)
396
- if (!cursorEl || !ctx.scrubberCursorPoint) return
397
- const { x, y } = ctx.scrubberCursorPoint
398
- cursorEl.style.transform = `translate3d(${x}px, ${y}px, 0px)`
399
- },
400
- setFormatterAndParser(ctx) {
401
- if (!ctx.locale) return
402
- ctx.formatter = createFormatter(ctx.locale, ctx.formatOptions)
403
- ctx.parser = createParser(ctx.locale, ctx.formatOptions)
404
- },
405
- },
406
- compareFns: {
407
- formatOptions: (a, b) => isEqual(a, b),
408
- scrubberCursorPoint: (a, b) => isEqual(a, b),
409
- },
410
- },
411
- )
412
- }
413
-
414
- const sync = {
415
- input(ctx: MachineContext, value: string) {
416
- const inputEl = dom.getInputEl(ctx)
417
- if (!inputEl) return
418
-
419
- // record cursor position before updating input value
420
- const sel = recordCursor(inputEl)
421
-
422
- // restore cursor position after updating input value
423
- raf(() => {
424
- dom.setValue(inputEl, value)
425
- restoreCursor(inputEl, sel)
426
- })
427
- },
428
- }
429
-
430
- const invoke = {
431
- onChange: (ctx: MachineContext) => {
432
- ctx.onValueChange?.({
433
- value: ctx.value,
434
- valueAsNumber: ctx.valueAsNumber,
435
- })
436
- },
437
- }
438
-
439
- const set = {
440
- value: (ctx: MachineContext, value: string) => {
441
- if (isEqual(ctx.value, value)) return
442
- ctx.value = value
443
- invoke.onChange(ctx)
444
- },
445
- }
@@ -1,34 +0,0 @@
1
- import { createProps } from "@zag-js/types"
2
- import { createSplitProps } from "@zag-js/utils"
3
- import type { UserDefinedContext } from "./number-input.types"
4
-
5
- export const props = createProps<UserDefinedContext>()([
6
- "allowMouseWheel",
7
- "allowOverflow",
8
- "clampValueOnBlur",
9
- "dir",
10
- "disabled",
11
- "focusInputOnChange",
12
- "form",
13
- "formatOptions",
14
- "getRootNode",
15
- "id",
16
- "ids",
17
- "inputMode",
18
- "invalid",
19
- "locale",
20
- "max",
21
- "min",
22
- "name",
23
- "onFocusChange",
24
- "onValueChange",
25
- "onValueInvalid",
26
- "pattern",
27
- "required",
28
- "readOnly",
29
- "spinOnPress",
30
- "step",
31
- "translations",
32
- "value",
33
- ])
34
- export const splitProps = createSplitProps<Partial<UserDefinedContext>>(props)