diff --git a/.changeset/cold-dodos-lay.md b/.changeset/cold-dodos-lay.md new file mode 100644 index 00000000000..3b8dfc55e9a --- /dev/null +++ b/.changeset/cold-dodos-lay.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Remove hidden password input from accessibility tree when hidden diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index 558ca7e689f..15b83cd4b0b 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -734,14 +734,26 @@ const InstantPasswordRow = ({ return ( ); diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx index 5ea22b49e40..993884a6dab 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -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(, { 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(, { 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 => {