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>(
|
const [rawJsonValue, setRawJsonValue] = useState<string>(
|
||||||
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
|
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
|
// Use a ref to manage debouncing timeouts to avoid parsing JSON
|
||||||
// on every keystroke which would be inefficient and error-prone
|
// on every keystroke which would be inefficient and error-prone
|
||||||
|
|
@ -134,6 +137,37 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
||||||
return !!jsonError;
|
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
|
// Debounce JSON parsing and parent updates to handle typing gracefully
|
||||||
const debouncedUpdateParent = useCallback(
|
const debouncedUpdateParent = useCallback(
|
||||||
(jsonString: string) => {
|
(jsonString: string) => {
|
||||||
|
|
@ -429,9 +463,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={(currentValue as number)?.toString() ?? ""}
|
value={getNumericDisplayValue(path, currentValue)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
|
updateNumericDraft(path, val);
|
||||||
if (!val && !isRequired) {
|
if (!val && !isRequired) {
|
||||||
handleFieldChange(path, undefined);
|
handleFieldChange(path, undefined);
|
||||||
} else {
|
} 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}
|
placeholder={propSchema.description}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
min={propSchema.minimum}
|
min={propSchema.minimum}
|
||||||
|
|
@ -453,9 +501,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
step="1"
|
||||||
value={(currentValue as number)?.toString() ?? ""}
|
value={getNumericDisplayValue(path, currentValue)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
|
updateNumericDraft(path, val);
|
||||||
if (!val && !isRequired) {
|
if (!val && !isRequired) {
|
||||||
handleFieldChange(path, undefined);
|
handleFieldChange(path, undefined);
|
||||||
} else {
|
} 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}
|
placeholder={propSchema.description}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
min={propSchema.minimum}
|
min={propSchema.minimum}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { describe, it, expect, jest } from "@jest/globals";
|
import { describe, it, expect, jest } from "@jest/globals";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm";
|
import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm";
|
||||||
import type { JsonSchemaType } from "@/utils/jsonUtils";
|
import type { JsonSchemaType } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
|
|
@ -402,6 +402,29 @@ describe("DynamicJsonForm Number Fields", () => {
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledWith(98.6);
|
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