From 1d7dbe6aea9e0d9e4e1b81e1d6d56a6bb58752af Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Sun, 14 Jun 2026 15:34:21 +0700 Subject: [PATCH] fix(form-core): keep onSubmit error when blurring an unchanged field The submit-error auto-clear in FieldApi.validateSync fired on every non-submit validation cause, including blur. As a result, focusing and leaving a field without editing it dropped a valid submit error. Gate the clear on a value `change` cause so the error is only removed when the user actually enters a new value, matching the documented intent of the block. Closes #1242 --- packages/form-core/src/FieldApi.ts | 7 ++- packages/form-core/tests/FieldApi.spec.ts | 58 +++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index b1125ea09..be8a8668e 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1347,14 +1347,17 @@ export class FieldApi< /** * when we have an error for onSubmit in the state, we want - * to clear the error as soon as the user enters a valid value in the field + * to clear the error as soon as the user enters a valid value in the field. + * This must only happen on a value `change` - clearing it on `blur` (or any + * other non-value cause) would wrongly drop the submit error when the user + * focuses and leaves the field without editing it. */ const submitErrKey = getErrorMapKey('submit') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.state.meta.errorMap?.[submitErrKey] && - cause !== 'submit' && + cause === 'change' && !hasErrored ) { this.setMeta((prev) => ({ diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 411885ce0..b9e682c7f 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -2098,6 +2098,64 @@ describe('field api', () => { expect(field.getMeta().errors).toStrictEqual(['first name is required']) }) + it('should not clear onSubmit errors on blur when the value did not change', async () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'firstName', + validators: { + onSubmit: ({ value }) => + value.length > 0 ? undefined : 'first name is required', + }, + }) + + field.mount() + + await form.handleSubmit() + expect(field.getMeta().errors).toStrictEqual(['first name is required']) + + // Blurring the field without changing its value must keep the submit error + field.handleBlur() + expect(field.getMeta().errors).toStrictEqual(['first name is required']) + expect(field.getMeta().isValid).toBe(false) + }) + + it('should clear onSubmit errors once a valid value is entered', async () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'firstName', + validators: { + onSubmit: ({ value }) => + value.length > 0 ? undefined : 'first name is required', + }, + }) + + field.mount() + + await form.handleSubmit() + expect(field.getMeta().errors).toStrictEqual(['first name is required']) + + // Entering a valid value clears the stale submit error + field.handleChange('John') + expect(field.getMeta().errors).toStrictEqual([]) + expect(field.getMeta().isValid).toBe(true) + }) + it('should show onMount errors', async () => { const form = new FormApi({ defaultValues: {