From 53f408375a082678adecb1f19f04955422078cab Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 17 Jun 2026 14:16:07 -0400 Subject: [PATCH 1/6] fix(ui): Ensure hidden passworld field is hidden from a11y tree --- packages/ui/src/components/SignIn/SignInStart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index 558ca7e689f..e9cdcc59d9d 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -742,6 +742,7 @@ const InstantPasswordRow = ({ onActionClicked={show ? onForgotPasswordClick : undefined} ref={ref} tabIndex={show ? undefined : -1} + aria-hidden={!show} /> ); From 66e171efaa765866387f7f137851edfaaead4bbf Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 17 Jun 2026 14:17:08 -0400 Subject: [PATCH 2/6] add changeset --- .changeset/cold-dodos-lay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-dodos-lay.md 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 From 515b4e03f33e3ab6d05682c108572f8d0f06c3a6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 17 Jun 2026 14:34:09 -0400 Subject: [PATCH 3/6] use inert --- packages/ui/src/components/SignIn/SignInStart.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index e9cdcc59d9d..6b21c9d5900 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -734,6 +734,10 @@ const InstantPasswordRow = ({ return ( ); From 7736ab88633d3a74f593d2e6f60a2de67be828bd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 17 Jun 2026 14:38:26 -0400 Subject: [PATCH 4/6] add test --- .../SignIn/__tests__/SignInStart.test.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx index 5ea22b49e40..5ed9ba9ff79 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -573,6 +573,54 @@ describe('SignInStart', () => { }); }); + describe('Instant password field visibility', () => { + it('hides the empty instant password field from the a11y tree via inert', 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('inert'); + // toggle button + input are removed from tab order / a11y tree while hidden + expect(passwordField).toHaveAttribute('tabindex', '-1'); + }); + + it('reveals the instant password field (clears inert) 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 until the autofill poll fires + expect(row).toHaveAttribute('inert'); + + await waitFor( + () => { + expect(row).not.toHaveAttribute('inert'); + }, + { 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 => { From 8c1cc92b1d87afff44082953906b4aa4d0232d3a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 18 Jun 2026 10:04:46 -0400 Subject: [PATCH 5/6] fix(ui): use display:none instead of inert to hide instant password row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inert blocks programmatic event dispatch, which broke the cache-components integration test (Playwright fill with force:true silently no-ops on inert elements, causing sign-in to submit without a password). display:none achieves the same a11y goal — removes the row from the accessibility tree — without blocking Playwright's CDP-level interaction. --- packages/ui/src/components/SignIn/SignInStart.tsx | 7 +------ .../SignIn/__tests__/SignInStart.test.tsx | 14 ++++++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index 6b21c9d5900..6eb5b053b7d 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -734,18 +734,13 @@ 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 5ed9ba9ff79..d81eaccbd51 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -573,8 +573,8 @@ describe('SignInStart', () => { }); }); - describe('Instant password field visibility', () => { - it('hides the empty instant password field from the a11y tree via inert', async () => { + describe('Instant password field a11y', () => { + it('hides the empty instant password field from the a11y tree via display:none', async () => { const { wrapper } = await createFixtures(f => { f.withEmailAddress(); f.withPassword({ required: true }); @@ -585,12 +585,10 @@ describe('SignInStart', () => { expect(passwordField).not.toBeNull(); const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement; - expect(row).toHaveAttribute('inert'); - // toggle button + input are removed from tab order / a11y tree while hidden - expect(passwordField).toHaveAttribute('tabindex', '-1'); + expect(row).not.toBeVisible(); }); - it('reveals the instant password field (clears inert) when the browser autofills it', async () => { + 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', @@ -608,11 +606,11 @@ describe('SignInStart', () => { const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement; // initially hidden until the autofill poll fires - expect(row).toHaveAttribute('inert'); + expect(row).not.toBeVisible(); await waitFor( () => { - expect(row).not.toHaveAttribute('inert'); + expect(row).toBeVisible(); }, { timeout: 2000 }, ); From cd5b434f2ce956540c856abf6e164efb6e1e0247 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 18 Jun 2026 10:56:56 -0400 Subject: [PATCH 6/6] fix(ui): restore autofill-compatible CSS hiding for instant password row Replace display:none with off-screen CSS + aria-hidden so the browser autofill engine can still detect and fill the hidden password input. Also restores tabIndex=-1 to block keyboard focus. Updates tests to assert on aria-hidden rather than toBeVisible(), since CSS-in-JS styles are not reflected in jsdom computed styles. --- packages/ui/src/components/SignIn/SignInStart.tsx | 15 ++++++++++++++- .../SignIn/__tests__/SignInStart.test.tsx | 10 +++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index 6eb5b053b7d..15b83cd4b0b 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -734,12 +734,25 @@ 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 d81eaccbd51..993884a6dab 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -574,7 +574,7 @@ describe('SignInStart', () => { }); describe('Instant password field a11y', () => { - it('hides the empty instant password field from the a11y tree via display:none', async () => { + 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 }); @@ -585,7 +585,7 @@ describe('SignInStart', () => { expect(passwordField).not.toBeNull(); const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement; - expect(row).not.toBeVisible(); + expect(row).toHaveAttribute('aria-hidden', 'true'); }); it('reveals the instant password field when the browser autofills it', async () => { @@ -605,12 +605,12 @@ describe('SignInStart', () => { const passwordField = container.querySelector('#password-field') as HTMLElement; const row = passwordField.closest('[class*="formFieldRow"]') as HTMLElement; - // initially hidden until the autofill poll fires - expect(row).not.toBeVisible(); + // initially hidden from a11y tree until the autofill poll fires + expect(row).toHaveAttribute('aria-hidden', 'true'); await waitFor( () => { - expect(row).toBeVisible(); + expect(row).not.toHaveAttribute('aria-hidden'); }, { timeout: 2000 }, );