Skip to content

Commit 72b07fb

Browse files
authored
fix(InputNumber.tsx): preserve leading zeros; fix test (#113)
1 parent e19c552 commit 72b07fb

4 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/antd/InputNumber.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from "react"
1+
import { useCallback, useRef } from "react"
22
import { ControlProps, RendererProps } from "@jsonforms/core"
33
import { InputNumber as AntdInputNumber } from "antd"
44
import { NumericControlOptions } from "../ui-schema"
@@ -12,12 +12,16 @@ import {
1212
type AntdInputNumberProps = React.ComponentProps<typeof AntdInputNumber>
1313
type InputNumberProps = AntdInputNumberProps & RendererProps & ControlProps
1414

15+
const hasLeadingZero = (value?: string) =>
16+
value && value.substring(0, 1) === "0"
17+
1518
export function InputNumber({
1619
handleChange,
1720
path,
1821
schema,
1922
...props
2023
}: InputNumberProps) {
24+
const incomingValue = useRef<string>()
2125
const ariaLabel = props.label || schema.description || "Value"
2226

2327
const defaultValue = schema.default as number | undefined
@@ -71,6 +75,18 @@ export function InputNumber({
7175
// TODO: Revist useCallback use - was meant to prevent re-renders
7276
const formatter = useCallback(
7377
(value?: string | number): string => {
78+
/**
79+
* See block comment in `parser()` below.
80+
*/
81+
if (
82+
value &&
83+
typeof incomingValue.current === "string" &&
84+
hasLeadingZero(incomingValue.current) &&
85+
!isNaN(parseFloat(incomingValue.current))
86+
) {
87+
return incomingValue.current
88+
}
89+
7490
if (value !== "" && value !== undefined) {
7591
if (isPercentage) {
7692
const valueFloat =
@@ -87,6 +103,21 @@ export function InputNumber({
87103
// TODO: Revist useCallback use - was meant to prevent re-renders
88104
const parser = useCallback(
89105
(value?: string): number | undefined => {
106+
/**
107+
* When a parser & formatter are both present it triggers a
108+
* double-render of the wrapped AntD InputNumber. This double-render
109+
* also triggers a double-set of the incoming value, which will format
110+
* and remove leading zeros. We'd like to preserve those leading zeros
111+
* if they're part of a valid number, because a user may simply want
112+
* to update a value like 30000 to 300 without having to retype the full
113+
* number.
114+
*
115+
* The incomingValue.current ref updates on every input value which allows
116+
* the subsequent formatter call to access the raw value and make decisions
117+
* based on whether or not it has a leading zero, while avoiding
118+
* any async issues caused by needing to set or wait for state changes.
119+
*/
120+
incomingValue.current = value
90121
const isNumeric = value ? !isNaN(Number(value)) : false
91122
if (isNumeric && value !== undefined) {
92123
if (isPercentage) {

src/controls/NumericControl.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
numericUISchemaWithRule,
1414
numericPriceSchema,
1515
numericUSDUISchema,
16+
numericLeadingZerosSchema,
1617
} from "../testSchemas/numericSchema"
1718
import { JSONFormData } from "../common/schema-derived-types"
1819

@@ -164,4 +165,25 @@ describe("NumericControl", () => {
164165
expect(input).toHaveValue("123") // it should be rounded to an integer
165166
},
166167
)
168+
169+
it("preserves leading zeros on valid numerical inputs", async () => {
170+
render({
171+
schema: numericLeadingZerosSchema,
172+
})
173+
174+
const input = screen.getByRole("spinbutton")
175+
expect(input).toHaveValue("3000")
176+
177+
await userEvent.click(input)
178+
await userEvent.keyboard(
179+
"[ArrowLeft][ArrowLeft][ArrowLeft][ArrowLeft][Delete]",
180+
)
181+
expect(input).toHaveValue("000")
182+
183+
await userEvent.keyboard("5")
184+
expect(input).toHaveValue("5000")
185+
186+
await userEvent.keyboard("[ArrowRight][ArrowRight]a")
187+
expect(input).toHaveValue("")
188+
})
167189
})

src/controls/ObjectArrayControl.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe("ObjectArrayControl", () => {
164164

165165
await screen.findByText("Asset")
166166
await userEvent.click(screen.getByText("Destroy me!"))
167-
await screen.findByText("No data")
167+
await screen.findAllByText("No data")
168168
})
169169
test("Object Array ensures one default item exists in the list if subschema is a combinator", async () => {
170170
strictRender({

src/testSchemas/numericSchema.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ export const numericTheNumberSchema = {
2525
required: ["numericValue"],
2626
} satisfies JSONSchema
2727

28+
export const numericLeadingZerosSchema = {
29+
type: "object",
30+
properties: {
31+
numericValue: {
32+
title: "Leading Zeros",
33+
type: "number",
34+
default: "3000",
35+
},
36+
},
37+
} satisfies JSONSchema
38+
2839
export const numericWeightSchema = {
2940
type: "object",
3041
properties: {

0 commit comments

Comments
 (0)