From b9a3a7afe47e91746c57370db1bc0b1367d39633 Mon Sep 17 00:00:00 2001 From: Kirill <50060130+noskovvkirill@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:14:22 -0400 Subject: [PATCH] Fix autofill submission and lock inputs while pending --- .../src/components/UnifiedAuthForm.test.ts | 43 +++++++++++++++++++ .../src/components/UnifiedAuthForm.tsx | 8 +++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts index 4a56bae..e9fce4b 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts @@ -163,3 +163,46 @@ describe("UnifiedAuthForm", () => { act(() => renderer.unmount()); }); }); + +describe("browser autofill and pending submission", () => { + test("submits current controls without requiring change events and locks both input variants", () => { + const original = globalThis.FormData; + let received: Record | undefined; + let renderer!: ReturnType; + const currentForm = {}; + // Model browser FormData: values can arrive without any React onChange. + globalThis.FormData = class { + constructor(form: unknown) { expect(form).toBe(currentForm); } + get(name: string) { return name === "password" ? "autofilled-password" : "autofilled-identifier"; } + } as unknown as typeof FormData; + const render = (isLoading: boolean) => createElement(AppearanceProvider, { + children: createElement(LocalizationProvider, { + children: createElement(UnifiedAuthForm, { + targetDomain: "example.com", isLoading, + fields: [ + { name: "identifier", label: "Identifier", type: "text" }, + { name: "password", label: "Password", type: "password" }, + ], + onSubmitFields: values => { received = values; }, + onSSOClick: () => {}, onMFASelect: () => {}, onSignInOptionSelect: () => {}, + }), + }), + }); + try { + act(() => { renderer = create(render(false)); }); + act(() => renderer.root.findByType("form").props.onSubmit({ preventDefault() {}, currentTarget: currentForm })); + expect(received).toEqual({ identifier: "autofilled-identifier", password: "autofilled-password" }); + act(() => renderer.update(render(true))); + for (const input of renderer.root.findAllByType("input")) { + expect(input.props.readOnly).toBe(true); + expect(input.props.disabled).not.toBe(true); + } + received = undefined; + act(() => renderer.root.findByType("form").props.onSubmit({ preventDefault() {}, currentTarget: currentForm })); + expect(received).toBeUndefined(); + } finally { + globalThis.FormData = original; + act(() => renderer.unmount()); + } + }); +}); diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx index 60103dc..373e747 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx @@ -250,7 +250,9 @@ export function UnifiedAuthForm({
) => { e.preventDefault(); - onSubmitFields(formData); + if (isLoading) return; + const submitted = new FormData(e.currentTarget); + onSubmitFields(Object.fromEntries(fields.map((field) => [field.name, String(submitted.get(field.name) ?? "")]))); }} {...slot("form", "kma-form")} > @@ -271,6 +273,7 @@ export function UnifiedAuthForm({ required={field.required} autoComplete={getAutocomplete(field)} aria-describedby={getDescriptionIds(field)} + readOnly={isLoading} value={formData[field.name] || ""} onChange={(e) => setFormData((prev) => ({ @@ -307,7 +310,8 @@ export function UnifiedAuthForm({ required={field.required} autoComplete={getAutocomplete(field)} aria-describedby={getDescriptionIds(field)} - value={formData[field.name] || ""} + readOnly={isLoading} + value={formData[field.name] || ""} onChange={(e) => setFormData((prev) => ({ ...prev,