Skip to content
Merged
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/cold-dodos-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Remove hidden password input from accessibility tree when hidden
16 changes: 14 additions & 2 deletions packages/ui/src/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -734,14 +734,26 @@ const InstantPasswordRow = ({
return (
<Form.ControlRow
elementId={field.id}
sx={show ? undefined : { position: 'absolute', opacity: 0, height: 0, pointerEvents: 'none', marginTop: '-1rem' }}
aria-hidden={show ? undefined : true}
sx={
show
? undefined
: {
position: 'absolute',
opacity: 0,
height: 0,
overflow: 'hidden',
pointerEvents: 'none',
marginTop: '-1rem',
}
}
>
<Form.PasswordInput
{...field.props}
actionLabel={show ? localizationKeys('formFieldAction__forgotPassword') : undefined}
onActionClicked={show ? onForgotPasswordClick : undefined}
ref={ref}
tabIndex={show ? undefined : -1}
ref={ref}
/>
</Form.ControlRow>
);
Expand Down
46 changes: 46 additions & 0 deletions packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,52 @@ describe('SignInStart', () => {
});
});

describe('Instant password field a11y', () => {
it('hides the empty instant password field from the a11y tree via aria-hidden', async () => {
const { wrapper } = await createFixtures(f => {
f.withEmailAddress();
f.withPassword({ required: true });
});
const { container } = render(<SignInStart />, { wrapper });

const passwordField = container.querySelector('#password-field') as HTMLElement;
expect(passwordField).not.toBeNull();

const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement;
expect(row).toHaveAttribute('aria-hidden', 'true');
});

it('reveals the instant password field when the browser autofills it', async () => {
// Simulate the browser's :autofill animation that the component polls for
mockGetComputedStyle.mockReturnValue({
animationName: 'onAutoFillStart',
pointerEvents: 'auto',
getPropertyValue: vi.fn().mockReturnValue(''),
});

const { wrapper } = await createFixtures(f => {
f.withEmailAddress();
f.withPassword({ required: true });
});
const { container } = render(<SignInStart />, { wrapper });

const passwordField = container.querySelector('#password-field') as HTMLElement;
const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement;

// initially hidden from a11y tree until the autofill poll fires
expect(row).toHaveAttribute('aria-hidden', 'true');

await waitFor(
() => {
expect(row).not.toHaveAttribute('aria-hidden');
},
{ timeout: 2000 },
);
// Forgot password action only renders once the field is shown
screen.getByText(/Forgot password/i);
});
});

describe('Session already exists error handling', () => {
it('redirects user when session_exists error is returned during sign-in', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
Expand Down
Loading