Skip to content

Commit 42f5b8e

Browse files
committed
fix: number-input
1 parent 1ca44d2 commit 42f5b8e

File tree

4 files changed

+114
-21
lines changed

4 files changed

+114
-21
lines changed

.changeset/friendly-pears-camp.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@zag-js/number-input": patch
3+
---
4+
5+
- Fixed issue where input element doesn't sync when `formatOptions` changes dynamically.
6+
- Ensure cursor position is preserved when `Enter` key is pressed and formatting is triggered.

examples/next-ts/pages/number-input-controlled.tsx

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,19 @@ import { normalizeProps, useMachine } from "@zag-js/react"
33
import { useId, useState } from "react"
44

55
export default function Page() {
6-
const [value, setValue] = useState("0")
6+
const [value, setValue] = useState("")
7+
const [formatOptions, setFormatOptions] = useState<Intl.NumberFormatOptions | undefined>(undefined)
8+
const [maxFractionDigits, setMaxFractionDigits] = useState<number | undefined>(undefined)
79

810
const service = useMachine(numberInput.machine, {
911
id: useId(),
1012
value: value,
11-
max: 10,
13+
max: 1000,
1214
min: 0,
13-
clampValueOnBlur: true,
15+
formatOptions:
16+
maxFractionDigits !== undefined ? { ...formatOptions, maximumFractionDigits: maxFractionDigits } : formatOptions,
1417
onValueChange(details) {
15-
if (details.valueAsNumber > 10) {
16-
setValue("10")
17-
} else if (details.valueAsNumber < 0) {
18-
setValue("0")
19-
} else {
20-
setValue(details.value)
21-
}
18+
setValue(details.value)
2219
},
2320
})
2421

@@ -27,10 +24,68 @@ export default function Page() {
2724
return (
2825
<>
2926
<main className="number-input">
27+
<h2>Controlled Number Input Testing</h2>
28+
29+
{/* Test buttons for external value changes */}
30+
<div style={{ marginBottom: "1rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
31+
<button data-testid="set-decimal" onClick={() => setValue("123.456789012345")}>
32+
Set High Precision (123.456789012345)
33+
</button>
34+
<button data-testid="set-simple" onClick={() => setValue("50.25")}>
35+
Set Simple (50.25)
36+
</button>
37+
<button data-testid="set-zero" onClick={() => setValue("0")}>
38+
Set Zero
39+
</button>
40+
<button data-testid="clear-value" onClick={() => setValue("")}>
41+
Clear (Uncontrolled)
42+
</button>
43+
</div>
44+
45+
{/* Format option controls */}
46+
<div style={{ marginBottom: "1rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
47+
<button
48+
data-testid="no-format"
49+
onClick={() => {
50+
setFormatOptions(undefined)
51+
setMaxFractionDigits(undefined)
52+
}}
53+
>
54+
No Format Options
55+
</button>
56+
<button
57+
data-testid="currency-format"
58+
onClick={() => {
59+
setFormatOptions({ style: "currency", currency: "USD" })
60+
setMaxFractionDigits(undefined)
61+
}}
62+
>
63+
Currency Format
64+
</button>
65+
<button
66+
data-testid="precision-2"
67+
onClick={() => {
68+
setFormatOptions(undefined)
69+
setMaxFractionDigits(2)
70+
}}
71+
>
72+
Max 2 Decimals
73+
</button>
74+
<button
75+
data-testid="precision-4"
76+
onClick={() => {
77+
setFormatOptions(undefined)
78+
setMaxFractionDigits(4)
79+
}}
80+
>
81+
Max 4 Decimals
82+
</button>
83+
</div>
84+
3085
<div {...api.getRootProps()}>
3186
<div data-testid="scrubber" {...api.getScrubberProps()} />
3287
<label data-testid="label" {...api.getLabelProps()}>
33-
Enter number (0-10, clamped during typing):
88+
Controlled Number Input (0-1000):
3489
</label>
3590
<div {...api.getControlProps()}>
3691
<button data-testid="dec-button" {...api.getDecrementTriggerProps()}>
@@ -43,14 +98,45 @@ export default function Page() {
4398
</div>
4499
</div>
45100

46-
<div style={{ marginTop: "1rem" }}>
47-
<strong>Controlled value:</strong> {value}
101+
{/* Status display */}
102+
<div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#f5f5f5", borderRadius: "4px" }}>
103+
<div>
104+
<strong>Controlled Value:</strong> "{value}"
105+
</div>
106+
<div>
107+
<strong>Is Controlled:</strong> {value !== "" ? "Yes" : "No"}
108+
</div>
109+
<div>
110+
<strong>Format Options:</strong> {formatOptions ? JSON.stringify(formatOptions) : "None"}
111+
</div>
112+
<div>
113+
<strong>Max Fraction Digits:</strong> {maxFractionDigits ?? "None"}
114+
</div>
115+
<div>
116+
<strong>Value As Number:</strong> {api.valueAsNumber}
117+
</div>
48118
</div>
49119

50-
<p style={{ marginTop: "1rem", color: "#666", fontSize: "0.875rem" }}>
51-
Try typing a value greater than 10 (e.g., "100" or "1000"). The input will be clamped back to "10" immediately
52-
during typing, not just on blur.
53-
</p>
120+
<div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e8f4fd", borderRadius: "4px" }}>
121+
<h3>Testing Instructions</h3>
122+
<ul style={{ fontSize: "0.875rem", lineHeight: "1.4" }}>
123+
<li>
124+
<strong>Controlled Mode:</strong> When you set a value using buttons above, input is controlled
125+
</li>
126+
<li>
127+
<strong>Uncontrolled Mode:</strong> When you clear the value, input becomes uncontrolled
128+
</li>
129+
<li>
130+
<strong>Precision Test:</strong> Set high precision value, then type to see how precision is preserved
131+
</li>
132+
<li>
133+
<strong>Format Test:</strong> Apply different formats to see controlled vs uncontrolled behavior
134+
</li>
135+
<li>
136+
<strong>External Changes:</strong> Use buttons to change value externally while typing
137+
</li>
138+
</ul>
139+
</div>
54140
</main>
55141
</>
56142
)

packages/machines/number-input/src/number-input.connect.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function connect<T extends PropTypes>(
172172

173173
const step = getEventStep(event) * prop("step")
174174

175-
const keyMap: EventKeyMap = {
175+
const keyMap: EventKeyMap<HTMLInputElement> = {
176176
ArrowUp() {
177177
send({ type: "INPUT.ARROW_UP", step })
178178
event.preventDefault()
@@ -191,8 +191,9 @@ export function connect<T extends PropTypes>(
191191
send({ type: "INPUT.END" })
192192
event.preventDefault()
193193
},
194-
Enter() {
195-
send({ type: "INPUT.ENTER" })
194+
Enter(event) {
195+
const selection = recordCursor(event.currentTarget, scope)
196+
send({ type: "INPUT.ENTER", selection })
196197
},
197198
}
198199

packages/machines/number-input/src/number-input.machine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const machine = createMachine({
101101
},
102102

103103
watch({ track, action, context, computed, prop }) {
104-
track([() => context.get("value"), () => prop("locale")], () => {
104+
track([() => context.get("value"), () => prop("locale"), () => JSON.stringify(prop("formatOptions"))], () => {
105105
action(["syncInputElement"])
106106
})
107107

0 commit comments

Comments
 (0)