docs: correct how a null reaches the number field

Copilot review: the comments offered "a nullable number the user cleared" as a
way to get an explicit `null` into this field. Clearing the box reports
`undefined` — `toNumericValue("")` says so, and an existing test pins it — so
the only way `null` arrives is from parent state: a value received from the
server, restored from a deep link, or written by a caller for a nullable
schema. Say that in all three places instead.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 01:08:40 -04:00
commit a945fb71c6
2 changed files with 17 additions and 9 deletions

View file

@ -1743,8 +1743,11 @@ describe("SchemaForm defaulted fields (#2026)", () => {
}); });
// An explicit `null` is a value, not an absent one, so a non-null default // An explicit `null` is a value, not an absent one, so a non-null default
// must not be substituted for it. Reachable for a nullable number field the // must not be substituted for it. Note the field itself never emits `null` —
// user cleared, or one the server sent as null. // clearing it reports `undefined` (pinned by "passes undefined to onChange
// when a number field is cleared") — so this arrives from parent state: a
// value received from the server, restored from a deep link, or written by a
// caller for a nullable schema.
it("shows an explicit null as empty, not as the default", () => { it("shows an explicit null as empty, not as the default", () => {
const schema: InspectorFormSchema = { const schema: InspectorFormSchema = {
type: "object", type: "object",

View file

@ -341,9 +341,9 @@ interface SchemaNumberInputProps {
* Raw, not `resolveValue`-substituted — see `SchemaJsonFieldProps.value`. * Raw, not `resolveValue`-substituted — see `SchemaJsonFieldProps.value`.
* *
* `null` is admitted alongside `undefined` because a nullable number schema * `null` is admitted alongside `undefined` because a nullable number schema
* produces it, and the two are not interchangeable here: `undefined` means * produces it — by way of parent state, never by clearing the box — and the
* "no value supplied" and takes `defaultValue`, while `null` is a value and * two are not interchangeable here: `undefined` means "no value supplied"
* displays as an empty box. * and takes `defaultValue`, while `null` is a value and displays empty.
*/ */
value: number | null | undefined; value: number | null | undefined;
/** The schema `default`, used to seed the draft only (#2026). May be `null`. */ /** The schema `default`, used to seed the draft only (#2026). May be `null`. */
@ -387,10 +387,15 @@ function SchemaNumberInput({
...inputProps ...inputProps
}: SchemaNumberInputProps) { }: SchemaNumberInputProps) {
// `undefined` means "no value supplied", which is what the default is for. // `undefined` means "no value supplied", which is what the default is for.
// An explicit `null` is a value — a nullable number the user cleared, or one // An explicit `null` is a value, so it must not be overwritten by a non-null
// the server sent — so it must not be overwritten by a non-null default; it // default; it displays as the empty box `resolveValue` produced before.
// displays as the empty box `resolveValue` produced before. Hence the // Hence the `undefined` test rather than `??`, matching `SchemaJsonField`.
// `undefined` test rather than `??`, matching `SchemaJsonField`. //
// Clearing the box does *not* produce that `null` — `toNumericValue("")`
// reports `undefined`, which is the behavior the "passes undefined to
// onChange when a number field is cleared" test pins. A `null` reaches this
// field only from parent state: a value received from the server, restored
// from a deep link, or written by a caller for a nullable schema.
const [draft, setDraft] = useState<string | number>( const [draft, setDraft] = useState<string | number>(
(value === undefined ? defaultValue : value) ?? "", (value === undefined ? defaultValue : value) ?? "",
); );