From e5eedde7a4cc8be04894d36911253ae66d5b6898 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Aamir <93091750+umeraamir69@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:53:28 -0400 Subject: [PATCH] Set autocomplete on SignIn and SignUp identifier fields. Email, phone, username, and name fields had no autocomplete tokens, so Safari and password managers would not offer the user's details. --- .changeset/signin-signup-autocomplete.md | 5 ++ .../ui/src/components/SignIn/SignInStart.tsx | 7 ++- .../SignIn/__tests__/SignInStart.test.tsx | 50 +++++++++++++++++++ .../ui/src/components/SignUp/SignUpForm.tsx | 5 ++ .../SignUp/__tests__/SignUpStart.test.tsx | 15 ++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 .changeset/signin-signup-autocomplete.md diff --git a/.changeset/signin-signup-autocomplete.md b/.changeset/signin-signup-autocomplete.md new file mode 100644 index 00000000000..f40a4920307 --- /dev/null +++ b/.changeset/signin-signup-autocomplete.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Sign-in and sign-up identifier fields now set standard `autocomplete` tokens so browsers and password managers can offer email, name, and phone. diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index 4e3ae9bf6ff..3a42ad78fa4 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -57,6 +57,11 @@ import { getSignUpAttributeFromIdentifier, } from './utils'; +function identifierAutoComplete(identifier: SignInStartIdentifier, webauthn: boolean): string { + const token = identifier === 'phone_number' ? 'tel' : identifier === 'email_address' ? 'email' : 'username'; + return webauthn ? `${token} webauthn` : token; +} + const useAutoFillPasskey = () => { const [isSupported, setIsSupported] = useState(false); const { navigate } = useRouter(); @@ -671,7 +676,7 @@ function SignInStartInternal(): JSX.Element { onActionClicked={switchToNextIdentifier} {...identifierFieldProps} autoFocus={shouldAutofocus} - autoComplete={isWebAuthnAutofillSupported ? 'webauthn' : undefined} + autoComplete={identifierAutoComplete(identifierAttribute, isWebAuthnAutofillSupported)} isLastAuthenticationStrategy={isIdentifierLastAuthenticationStrategy} /> diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx index 1da263fbd58..365659b65e7 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -76,6 +76,37 @@ describe('SignInStart', () => { screen.getByText(/email address/i); }); + it('sets autocomplete=email on an email-only identifier field', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress(); + }); + + render(, { wrapper }); + expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute('autocomplete', 'email'); + }); + + it('sets autocomplete=username when email and username are both enabled', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress(); + f.withUsername(); + }); + + render(, { wrapper }); + expect(screen.getByRole('textbox', { name: /email address or username/i })).toHaveAttribute( + 'autocomplete', + 'username', + ); + }); + + it('sets autocomplete=tel on a phone identifier field', async () => { + const { wrapper } = await createFixtures(f => { + f.withPhoneNumber(); + }); + + render(, { wrapper }); + expect(screen.getByRole('textbox', { name: /phone number/i })).toHaveAttribute('autocomplete', 'tel'); + }); + it('enables login with username', async () => { const { wrapper } = await createFixtures(f => { f.withUsername(); @@ -131,6 +162,25 @@ describe('SignInStart', () => { screen.getByText('Use passkey instead'); }); + it('puts webauthn after the field name when passkey autofill is on', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress(); + f.withPasskey(); + f.withPasskeySettings({ + allow_autofill: true, + show_sign_in_button: false, + }); + }); + + render(, { wrapper }); + await waitFor(() => { + expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute( + 'autocomplete', + 'email webauthn', + ); + }); + }); + it('enables login with passkey via autofill', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress(); diff --git a/packages/ui/src/components/SignUp/SignUpForm.tsx b/packages/ui/src/components/SignUp/SignUpForm.tsx index e36a979e464..649b1817c46 100644 --- a/packages/ui/src/components/SignUp/SignUpForm.tsx +++ b/packages/ui/src/components/SignUp/SignUpForm.tsx @@ -59,6 +59,7 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.firstName.props} isRequired={fields.firstName?.required} isOptional={!fields.firstName?.required} + autoComplete='given-name' /> )} {shouldShow('lastName') && ( @@ -66,6 +67,7 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.lastName.props} isRequired={fields.lastName?.required} isOptional={!fields.lastName?.required} + autoComplete='family-name' /> )} @@ -76,6 +78,7 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.username.props} isRequired={fields.username?.required} isOptional={!fields.username?.required} + autoComplete='username' /> )} @@ -86,6 +89,7 @@ export const SignUpForm = (props: SignUpFormProps) => { isRequired={fields.emailAddress?.required} isOptional={!fields.emailAddress?.required} isDisabled={fields.emailAddress?.disabled} + autoComplete='email' actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_phone') : undefined} onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('phoneNumber') : undefined} /> @@ -97,6 +101,7 @@ export const SignUpForm = (props: SignUpFormProps) => { {...formState.phoneNumber.props} isRequired={fields.phoneNumber?.required} isOptional={!fields.phoneNumber?.required} + autoComplete='tel' actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_email') : undefined} onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('emailAddress') : undefined} /> diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx index eb5fb3f4739..f7adfc1457b 100644 --- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx @@ -31,6 +31,21 @@ describe('SignUpStart', () => { screen.getByText('Email address'); }); + it('sets standard autocomplete tokens on sign-up fields', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ required: true }); + f.withPhoneNumber({ required: true }); + f.withUsername({ required: true }); + f.withName({ required: true }); + }); + render(, { wrapper }); + expect(screen.getByRole('textbox', { name: 'First name' })).toHaveAttribute('autocomplete', 'given-name'); + expect(screen.getByRole('textbox', { name: 'Last name' })).toHaveAttribute('autocomplete', 'family-name'); + expect(screen.getByLabelText('Email address', { selector: 'input' })).toHaveAttribute('autocomplete', 'email'); + expect(screen.getByLabelText('Phone number', { selector: 'input' })).toHaveAttribute('autocomplete', 'tel'); + expect(screen.getByRole('textbox', { name: 'Username' })).toHaveAttribute('autocomplete', 'username'); + }); + it('enables sign up with phone number', async () => { const { wrapper } = await createFixtures(f => { f.withPhoneNumber({ required: true });