Skip to content

Commit e5eedde

Browse files
committed
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.
1 parent 141916c commit e5eedde

5 files changed

Lines changed: 81 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': patch
3+
---
4+
5+
Sign-in and sign-up identifier fields now set standard `autocomplete` tokens so browsers and password managers can offer email, name, and phone.

‎packages/ui/src/components/SignIn/SignInStart.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ import {
5757
getSignUpAttributeFromIdentifier,
5858
} from './utils';
5959

60+
function identifierAutoComplete(identifier: SignInStartIdentifier, webauthn: boolean): string {
61+
const token = identifier === 'phone_number' ? 'tel' : identifier === 'email_address' ? 'email' : 'username';
62+
return webauthn ? `${token} webauthn` : token;
63+
}
64+
6065
const useAutoFillPasskey = () => {
6166
const [isSupported, setIsSupported] = useState(false);
6267
const { navigate } = useRouter();
@@ -671,7 +676,7 @@ function SignInStartInternal(): JSX.Element {
671676
onActionClicked={switchToNextIdentifier}
672677
{...identifierFieldProps}
673678
autoFocus={shouldAutofocus}
674-
autoComplete={isWebAuthnAutofillSupported ? 'webauthn' : undefined}
679+
autoComplete={identifierAutoComplete(identifierAttribute, isWebAuthnAutofillSupported)}
675680
isLastAuthenticationStrategy={isIdentifierLastAuthenticationStrategy}
676681
/>
677682
</Form.ControlRow>

‎packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,37 @@ describe('SignInStart', () => {
7676
screen.getByText(/email address/i);
7777
});
7878

79+
it('sets autocomplete=email on an email-only identifier field', async () => {
80+
const { wrapper } = await createFixtures(f => {
81+
f.withEmailAddress();
82+
});
83+
84+
render(<SignInStart />, { wrapper });
85+
expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute('autocomplete', 'email');
86+
});
87+
88+
it('sets autocomplete=username when email and username are both enabled', async () => {
89+
const { wrapper } = await createFixtures(f => {
90+
f.withEmailAddress();
91+
f.withUsername();
92+
});
93+
94+
render(<SignInStart />, { wrapper });
95+
expect(screen.getByRole('textbox', { name: /email address or username/i })).toHaveAttribute(
96+
'autocomplete',
97+
'username',
98+
);
99+
});
100+
101+
it('sets autocomplete=tel on a phone identifier field', async () => {
102+
const { wrapper } = await createFixtures(f => {
103+
f.withPhoneNumber();
104+
});
105+
106+
render(<SignInStart />, { wrapper });
107+
expect(screen.getByRole('textbox', { name: /phone number/i })).toHaveAttribute('autocomplete', 'tel');
108+
});
109+
79110
it('enables login with username', async () => {
80111
const { wrapper } = await createFixtures(f => {
81112
f.withUsername();
@@ -131,6 +162,25 @@ describe('SignInStart', () => {
131162
screen.getByText('Use passkey instead');
132163
});
133164

165+
it('puts webauthn after the field name when passkey autofill is on', async () => {
166+
const { wrapper } = await createFixtures(f => {
167+
f.withEmailAddress();
168+
f.withPasskey();
169+
f.withPasskeySettings({
170+
allow_autofill: true,
171+
show_sign_in_button: false,
172+
});
173+
});
174+
175+
render(<SignInStart />, { wrapper });
176+
await waitFor(() => {
177+
expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute(
178+
'autocomplete',
179+
'email webauthn',
180+
);
181+
});
182+
});
183+
134184
it('enables login with passkey via autofill', async () => {
135185
const { wrapper, fixtures } = await createFixtures(f => {
136186
f.withEmailAddress();

‎packages/ui/src/components/SignUp/SignUpForm.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ export const SignUpForm = (props: SignUpFormProps) => {
5959
{...formState.firstName.props}
6060
isRequired={fields.firstName?.required}
6161
isOptional={!fields.firstName?.required}
62+
autoComplete='given-name'
6263
/>
6364
)}
6465
{shouldShow('lastName') && (
6566
<Form.PlainInput
6667
{...formState.lastName.props}
6768
isRequired={fields.lastName?.required}
6869
isOptional={!fields.lastName?.required}
70+
autoComplete='family-name'
6971
/>
7072
)}
7173
</Form.ControlRow>
@@ -76,6 +78,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
7678
{...formState.username.props}
7779
isRequired={fields.username?.required}
7880
isOptional={!fields.username?.required}
81+
autoComplete='username'
7982
/>
8083
</Form.ControlRow>
8184
)}
@@ -86,6 +89,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
8689
isRequired={fields.emailAddress?.required}
8790
isOptional={!fields.emailAddress?.required}
8891
isDisabled={fields.emailAddress?.disabled}
92+
autoComplete='email'
8993
actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_phone') : undefined}
9094
onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('phoneNumber') : undefined}
9195
/>
@@ -97,6 +101,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
97101
{...formState.phoneNumber.props}
98102
isRequired={fields.phoneNumber?.required}
99103
isOptional={!fields.phoneNumber?.required}
104+
autoComplete='tel'
100105
actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_email') : undefined}
101106
onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('emailAddress') : undefined}
102107
/>

‎packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ describe('SignUpStart', () => {
3131
screen.getByText('Email address');
3232
});
3333

34+
it('sets standard autocomplete tokens on sign-up fields', async () => {
35+
const { wrapper } = await createFixtures(f => {
36+
f.withEmailAddress({ required: true });
37+
f.withPhoneNumber({ required: true });
38+
f.withUsername({ required: true });
39+
f.withName({ required: true });
40+
});
41+
render(<SignUpStart />, { wrapper });
42+
expect(screen.getByRole('textbox', { name: 'First name' })).toHaveAttribute('autocomplete', 'given-name');
43+
expect(screen.getByRole('textbox', { name: 'Last name' })).toHaveAttribute('autocomplete', 'family-name');
44+
expect(screen.getByLabelText('Email address', { selector: 'input' })).toHaveAttribute('autocomplete', 'email');
45+
expect(screen.getByLabelText('Phone number', { selector: 'input' })).toHaveAttribute('autocomplete', 'tel');
46+
expect(screen.getByRole('textbox', { name: 'Username' })).toHaveAttribute('autocomplete', 'username');
47+
});
48+
3449
it('enables sign up with phone number', async () => {
3550
const { wrapper } = await createFixtures(f => {
3651
f.withPhoneNumber({ required: true });

0 commit comments

Comments
 (0)