Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/signin-signup-autocomplete.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 6 additions & 1 deletion packages/ui/src/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -671,7 +676,7 @@ function SignInStartInternal(): JSX.Element {
onActionClicked={switchToNextIdentifier}
{...identifierFieldProps}
autoFocus={shouldAutofocus}
autoComplete={isWebAuthnAutofillSupported ? 'webauthn' : undefined}
autoComplete={identifierAutoComplete(identifierAttribute, isWebAuthnAutofillSupported)}
isLastAuthenticationStrategy={isIdentifierLastAuthenticationStrategy}
/>
</Form.ControlRow>
Expand Down
50 changes: 50 additions & 0 deletions packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SignInStart />, { 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(<SignInStart />, { 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(<SignInStart />, { 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();
Expand Down Expand Up @@ -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(<SignInStart />, { 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();
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/components/SignUp/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.firstName.props}
isRequired={fields.firstName?.required}
isOptional={!fields.firstName?.required}
autoComplete='given-name'
/>
)}
{shouldShow('lastName') && (
<Form.PlainInput
{...formState.lastName.props}
isRequired={fields.lastName?.required}
isOptional={!fields.lastName?.required}
autoComplete='family-name'
/>
)}
</Form.ControlRow>
Expand All @@ -76,6 +78,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.username.props}
isRequired={fields.username?.required}
isOptional={!fields.username?.required}
autoComplete='username'
/>
</Form.ControlRow>
)}
Expand All @@ -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}
/>
Expand All @@ -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}
/>
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SignUpStart />, { 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 });
Expand Down