Fix decimal input handling in DynamicJsonForm number fields
This commit is contained in:
parent
db0827b246
commit
fdbf896a04
2 changed files with 88 additions and 3 deletions
|
|
@ -125,6 +125,9 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
const [rawJsonValue, setRawJsonValue] = useState<string>(
|
||||
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
|
||||
);
|
||||
const [numericInputDrafts, setNumericInputDrafts] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
// Use a ref to manage debouncing timeouts to avoid parsing JSON
|
||||
// on every keystroke which would be inefficient and error-prone
|
||||
|
|
@ -134,6 +137,37 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
return !!jsonError;
|
||||
};
|
||||
|
||||
const getPathKey = (path: string[]) =>
|
||||
path.length === 0 ? "$root" : path.join(".");
|
||||
|
||||
const getNumericDisplayValue = (
|
||||
path: string[],
|
||||
currentValue: JsonValue,
|
||||
): string => {
|
||||
const pathKey = getPathKey(path);
|
||||
if (Object.prototype.hasOwnProperty.call(numericInputDrafts, pathKey)) {
|
||||
return numericInputDrafts[pathKey];
|
||||
}
|
||||
return typeof currentValue === "number" ? currentValue.toString() : "";
|
||||
};
|
||||
|
||||
const updateNumericDraft = (path: string[], draftValue: string) => {
|
||||
const pathKey = getPathKey(path);
|
||||
setNumericInputDrafts((prev) => ({ ...prev, [pathKey]: draftValue }));
|
||||
};
|
||||
|
||||
const clearNumericDraft = (path: string[]) => {
|
||||
const pathKey = getPathKey(path);
|
||||
setNumericInputDrafts((prev) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(prev, pathKey)) {
|
||||
return prev;
|
||||
}
|
||||
const next = { ...prev };
|
||||
delete next[pathKey];
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Debounce JSON parsing and parent updates to handle typing gracefully
|
||||
const debouncedUpdateParent = useCallback(
|
||||
(jsonString: string) => {
|
||||
|
|
@ -429,9 +463,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={(currentValue as number)?.toString() ?? ""}
|
||||
value={getNumericDisplayValue(path, currentValue)}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
updateNumericDraft(path, val);
|
||||
if (!val && !isRequired) {
|
||||
handleFieldChange(path, undefined);
|
||||
} else {
|
||||
|
|
@ -441,6 +476,19 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value;
|
||||
if (!val) {
|
||||
clearNumericDraft(path);
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(val);
|
||||
if (!isNaN(num)) {
|
||||
handleFieldChange(path, num);
|
||||
}
|
||||
clearNumericDraft(path);
|
||||
}}
|
||||
placeholder={propSchema.description}
|
||||
required={isRequired}
|
||||
min={propSchema.minimum}
|
||||
|
|
@ -453,9 +501,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
value={(currentValue as number)?.toString() ?? ""}
|
||||
value={getNumericDisplayValue(path, currentValue)}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
updateNumericDraft(path, val);
|
||||
if (!val && !isRequired) {
|
||||
handleFieldChange(path, undefined);
|
||||
} else {
|
||||
|
|
@ -465,6 +514,19 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
|||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value;
|
||||
if (!val) {
|
||||
clearNumericDraft(path);
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(val);
|
||||
if (!isNaN(num) && Number.isInteger(num)) {
|
||||
handleFieldChange(path, num);
|
||||
}
|
||||
clearNumericDraft(path);
|
||||
}}
|
||||
placeholder={propSchema.description}
|
||||
required={isRequired}
|
||||
min={propSchema.minimum}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { describe, it, expect, jest } from "@jest/globals";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm";
|
||||
import type { JsonSchemaType } from "@/utils/jsonUtils";
|
||||
|
||||
|
|
@ -402,6 +402,29 @@ describe("DynamicJsonForm Number Fields", () => {
|
|||
|
||||
expect(onChange).toHaveBeenCalledWith(98.6);
|
||||
});
|
||||
|
||||
it("should preserve decimal zero while typing", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "number",
|
||||
description: "Coordinate",
|
||||
};
|
||||
|
||||
const WrappedForm = () => {
|
||||
const [value, setValue] = useState<number>(0);
|
||||
return (
|
||||
<DynamicJsonForm schema={schema} value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
|
||||
render(<WrappedForm />);
|
||||
const input = screen.getByRole("spinbutton") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "-74.0" } });
|
||||
expect(input.value).toBe("-74.0");
|
||||
|
||||
fireEvent.change(input, { target: { value: "-74.01" } });
|
||||
expect(input.value).toBe("-74.01");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue