@zag-js/number-input 0.30.0 → 0.31.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.
@@ -1,5 +1,4 @@
1
- import { NumberParser } from "@internationalized/number"
2
- import { choose, createMachine, guards, ref } from "@zag-js/core"
1
+ import { choose, createMachine, guards } from "@zag-js/core"
3
2
  import { addDomEvent, requestPointerLock } from "@zag-js/dom-event"
4
3
  import { isSafari, raf } from "@zag-js/dom-query"
5
4
  import { trackFormControl } from "@zag-js/form-utils"
@@ -9,6 +8,7 @@ import { callAll, compact, isEqual } from "@zag-js/utils"
9
8
  import { recordCursor, restoreCursor } from "./cursor"
10
9
  import { dom } from "./number-input.dom"
11
10
  import type { MachineContext, MachineState, UserDefinedContext } from "./number-input.types"
11
+ import { createFormatter, createParser, formatValue, parseValue } from "./number-input.utils"
12
12
 
13
13
  const { not, and } = guards
14
14
 
@@ -48,19 +48,16 @@ export function machine(userContext: UserDefinedContext) {
48
48
 
49
49
  computed: {
50
50
  isRtl: (ctx) => ctx.dir === "rtl",
51
- valueAsNumber: (ctx) => ctx.parser.parse(String(ctx.value)) ?? 0,
52
- isAtMin: (ctx) => isAtMin(ctx.value, ctx),
53
- isAtMax: (ctx) => isAtMax(ctx.value, ctx),
54
- isOutOfRange: (ctx) => !isWithinRange(ctx.value, ctx),
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),
55
56
  isValueEmpty: (ctx) => ctx.value === "",
56
57
  isDisabled: (ctx) => !!ctx.disabled || ctx.fieldsetDisabled,
57
58
  canIncrement: (ctx) => ctx.allowOverflow || !ctx.isAtMax,
58
59
  canDecrement: (ctx) => ctx.allowOverflow || !ctx.isAtMin,
59
60
  valueText: (ctx) => ctx.translations.valueText?.(ctx.value),
60
- formattedValue: (ctx) => {
61
- if (Number.isNaN(ctx.valueAsNumber)) return ctx.value
62
- return ctx.formatter.format(ctx.valueAsNumber)
63
- },
64
61
  },
65
62
 
66
63
  watch: {
@@ -71,13 +68,11 @@ export function machine(userContext: UserDefinedContext) {
71
68
  scrubberCursorPoint: ["setVirtualCursorPosition"],
72
69
  },
73
70
 
74
- entry: ["syncInputValue"],
75
-
76
71
  activities: ["trackFormControl"],
77
72
 
78
73
  on: {
79
74
  "VALUE.SET": {
80
- actions: ["setValue", "clampValue", "setHintToSet"],
75
+ actions: ["setRawValue", "setHintToSet"],
81
76
  },
82
77
  "VALUE.CLEAR": {
83
78
  actions: ["clearValue"],
@@ -142,11 +137,11 @@ export function machine(userContext: UserDefinedContext) {
142
137
  {
143
138
  guard: and("clampValueOnBlur", not("isInRange")),
144
139
  target: "idle",
145
- actions: ["clampValue", "syncInputElement", "clearHint", "invokeOnBlur"],
140
+ actions: ["setClampedValue", "clearHint", "invokeOnBlur"],
146
141
  },
147
142
  {
148
143
  target: "idle",
149
- actions: ["syncInputElement", "invokeOnBlur"],
144
+ actions: ["setFormattedValue", "clearHint", "invokeOnBlur"],
150
145
  },
151
146
  ],
152
147
  "INPUT.COMPOSITION_START": {
@@ -318,16 +313,22 @@ export function machine(userContext: UserDefinedContext) {
318
313
  raf(() => inputEl?.focus({ preventScroll: true }))
319
314
  },
320
315
  increment(ctx, evt) {
321
- const nextValue = increment(ctx.value, evt.step ?? ctx.step)
322
- set.value(ctx, String(clamp(nextValue, ctx)))
316
+ const nextValue = increment(ctx.valueAsNumber, evt.step ?? ctx.step)
317
+ const value = ctx.formatter.format(clamp(nextValue, ctx))
318
+ set.value(ctx, value)
323
319
  },
324
320
  decrement(ctx, evt) {
325
- const nextValue = decrement(ctx.value, evt.step ?? ctx.step)
326
- set.value(ctx, String(clamp(nextValue, ctx)))
321
+ const nextValue = decrement(ctx.valueAsNumber, evt.step ?? ctx.step)
322
+ const value = ctx.formatter.format(clamp(nextValue, ctx))
323
+ set.value(ctx, value)
327
324
  },
328
- clampValue(ctx) {
329
- const nextValue = clamp(ctx.value, ctx)
330
- set.value(ctx, String(nextValue))
325
+ setClampedValue(ctx) {
326
+ const nextValue = clamp(ctx.valueAsNumber, ctx)
327
+ set.value(ctx, ctx.formatter.format(nextValue))
328
+ },
329
+ setRawValue(ctx, evt) {
330
+ const value = ctx.formatter.format(clamp(evt.value, ctx))
331
+ set.value(ctx, value)
331
332
  },
332
333
  setValue(ctx, evt) {
333
334
  const value = evt.target?.value ?? evt.value
@@ -337,10 +338,12 @@ export function machine(userContext: UserDefinedContext) {
337
338
  set.value(ctx, "")
338
339
  },
339
340
  incrementToMax(ctx) {
340
- set.value(ctx, String(ctx.max))
341
+ const value = ctx.formatter.format(ctx.max)
342
+ set.value(ctx, value)
341
343
  },
342
344
  decrementToMin(ctx) {
343
- set.value(ctx, String(ctx.min))
345
+ const value = ctx.formatter.format(ctx.min)
346
+ set.value(ctx, value)
344
347
  },
345
348
  setHint(ctx, evt) {
346
349
  ctx.hint = evt.hint
@@ -375,22 +378,12 @@ export function machine(userContext: UserDefinedContext) {
375
378
  })
376
379
  },
377
380
  syncInputElement(ctx, evt) {
378
- const inputEl = dom.getInputEl(ctx)
379
- const inputValue = evt.type.endsWith("CHANGE") ? ctx.value : ctx.formattedValue
380
-
381
- // record cursor position before updating input value
382
- const sel = recordCursor(inputEl!)
383
-
384
- // restore cursor position after updating input value
385
- raf(() => {
386
- dom.setValue(inputEl, inputValue)
387
- restoreCursor(inputEl!, sel)
388
- })
381
+ const value = evt.type.endsWith("CHANGE") ? ctx.value : ctx.formattedValue
382
+ sync.input(ctx, value)
389
383
  },
390
- syncInputValue(ctx) {
391
- const inputEl = dom.getInputEl(ctx)
392
- if (!inputEl || inputEl.value == ctx.value) return
393
- set.value(ctx, ctx.formattedValue)
384
+ setFormattedValue(ctx) {
385
+ const value = ctx.formatter.format(ctx.valueAsNumber)
386
+ set.value(ctx, value)
394
387
  },
395
388
  setCursorPoint(ctx, evt) {
396
389
  ctx.scrubberCursorPoint = evt.point
@@ -418,25 +411,26 @@ export function machine(userContext: UserDefinedContext) {
418
411
  },
419
412
  compareFns: {
420
413
  formatOptions: (a, b) => isEqual(a, b),
414
+ scrubberCursorPoint: (a, b) => isEqual(a, b),
421
415
  },
422
416
  },
423
417
  )
424
418
  }
425
419
 
426
- const defaultFormatOptions: Intl.NumberFormatOptions = {
427
- style: "decimal",
428
- minimumFractionDigits: 0,
429
- maximumFractionDigits: 20,
430
- }
420
+ const sync = {
421
+ input(ctx: MachineContext, value: string) {
422
+ const inputEl = dom.getInputEl(ctx)
423
+ if (!inputEl) return
431
424
 
432
- const createFormatter = (locale: string, options: Intl.NumberFormatOptions = {}) => {
433
- const formatOptions = Object.assign({}, defaultFormatOptions, options)
434
- return ref(new Intl.NumberFormat(locale, formatOptions))
435
- }
425
+ // record cursor position before updating input value
426
+ const sel = recordCursor(inputEl)
436
427
 
437
- const createParser = (locale: string, options: Intl.NumberFormatOptions = {}) => {
438
- const formatOptions = Object.assign({}, defaultFormatOptions, options)
439
- return ref(new NumberParser(locale, formatOptions))
428
+ // restore cursor position after updating input value
429
+ raf(() => {
430
+ dom.setValue(inputEl, value)
431
+ restoreCursor(inputEl, sel)
432
+ })
433
+ },
440
434
  }
441
435
 
442
436
  const invoke = {
@@ -283,7 +283,7 @@ export interface MachineApi<T extends PropTypes = PropTypes> {
283
283
  /**
284
284
  * Function to set the value of the input.
285
285
  */
286
- setValue(value: string | number): void
286
+ setValue(value: number): void
287
287
  /**
288
288
  * Function to clear the value of the input.
289
289
  */
@@ -0,0 +1,32 @@
1
+ import { NumberParser } from "@internationalized/number"
2
+ import { ref } from "@zag-js/core"
3
+ import type { MachineContext } from "./number-input.types"
4
+
5
+ const defaultFormatOptions: Intl.NumberFormatOptions = {
6
+ style: "decimal",
7
+ minimumFractionDigits: 0,
8
+ maximumFractionDigits: 20,
9
+ }
10
+
11
+ export const createFormatter = (locale: string, options: Intl.NumberFormatOptions = {}) => {
12
+ const formatOptions = Object.assign({}, defaultFormatOptions, options)
13
+ return ref(new Intl.NumberFormat(locale, formatOptions))
14
+ }
15
+
16
+ export const createParser = (locale: string, options: Intl.NumberFormatOptions = {}) => {
17
+ const formatOptions = Object.assign({}, defaultFormatOptions, options)
18
+ return ref(new NumberParser(locale, formatOptions))
19
+ }
20
+
21
+ export const parseValue = (ctx: MachineContext, value: string) => {
22
+ return ctx.parser.parse(String(value))
23
+ }
24
+
25
+ export const nan = (value: any, fallback: any) => {
26
+ return Number.isNaN(value) ? fallback : value
27
+ }
28
+
29
+ export const formatValue = (ctx: MachineContext, value: number): string => {
30
+ if (Number.isNaN(value)) return ""
31
+ return ctx.formatter.format(value)
32
+ }