From 99f7f4da6d835eecadf11b325b719daa4f6951c2 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 25 Sep 2026 10:35:16 -0400 Subject: [PATCH 1/2] fix(ui): pass boolean inert for interactive captcha Co-authored-by: Arjun Vijay Prakash --- .changeset/fix-ui-captcha-inert-boolean.md | 5 +++++ packages/ui/src/components/SignIn/SignInStart.tsx | 4 ++-- .../ui/src/components/SignIn/__tests__/SignInStart.test.tsx | 1 + packages/ui/src/components/SignUp/SignUpStart.tsx | 4 ++-- .../ui/src/components/SignUp/__tests__/SignUpStart.test.tsx | 1 + 5 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-ui-captcha-inert-boolean.md diff --git a/.changeset/fix-ui-captcha-inert-boolean.md b/.changeset/fix-ui-captcha-inert-boolean.md new file mode 100644 index 00000000000..ce5ebc6061a --- /dev/null +++ b/.changeset/fix-ui-captcha-inert-boolean.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Use a React 19-safe boolean `inert` value when the sign-in or sign-up captcha becomes interactive, so the form is actually inert and the empty-string warning is gone. diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx index d2115a3e575..71b14ab903b 100644 --- a/packages/ui/src/components/SignIn/SignInStart.tsx +++ b/packages/ui/src/components/SignIn/SignInStart.tsx @@ -1,4 +1,5 @@ import { getAlternativePhoneCodeProviderData } from '@clerk/shared/alternativePhoneCode'; +import { inertProps } from '@clerk/shared/inert'; import { ERROR_CODES, SIGN_UP_MODES } from '@clerk/shared/internal/clerk-js/constants'; import { clerkInvalidFAPIResponse } from '@clerk/shared/internal/clerk-js/errors'; import { getClerkQueryParam, removeClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams'; @@ -644,8 +645,7 @@ function SignInStartInternal(): JSX.Element { { simulateCaptchaInteractive(getCaptcha()); await waitFor(() => expect(google.closest('[inert]')).not.toBeNull()); + expect(google.closest('[inert]')?.hasAttribute('inert')).toBe(true); // The captcha widget and the header stay outside the inert subtree. expect(getCaptcha().closest('[inert]')).toBeNull(); expect(screen.getByRole('heading').closest('[inert]')).toBeNull(); diff --git a/packages/ui/src/components/SignUp/SignUpStart.tsx b/packages/ui/src/components/SignUp/SignUpStart.tsx index e565ea575f3..579e0c22f5d 100644 --- a/packages/ui/src/components/SignUp/SignUpStart.tsx +++ b/packages/ui/src/components/SignUp/SignUpStart.tsx @@ -1,5 +1,6 @@ import { getAlternativePhoneCodeProviderData } from '@clerk/shared/alternativePhoneCode'; import { isClerkAPIResponseError } from '@clerk/shared/error'; +import { inertProps } from '@clerk/shared/inert'; import { ERROR_CODES, SIGN_UP_MODES } from '@clerk/shared/internal/clerk-js/constants'; import { getClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams'; import { useClerk } from '@clerk/shared/react'; @@ -421,8 +422,7 @@ function SignUpStartInternal(): JSX.Element { direction='col' elementDescriptor={descriptors.main} gap={6} - // @ts-ignore - `inert` is not yet in the installed React types - inert={captchaIsInteractive ? '' : undefined} + {...inertProps(captchaIsInteractive)} // `display:none` (not `visibility:hidden`) so the collapsed column leaves flex flow and // contributes no `gap` gutter to `Card.Content` — otherwise it injects empty space above // the spotlighted captcha. Subtree stays mounted (form state preserved); `inert` is then diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx index eb5fb3f4739..cd9a0dca1d6 100644 --- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx @@ -639,6 +639,7 @@ describe('SignUpStart', () => { simulateCaptchaInteractive(getCaptcha()); await waitFor(() => expect(google.closest('[inert]')).not.toBeNull()); + expect(google.closest('[inert]')?.hasAttribute('inert')).toBe(true); expect(getCaptcha().closest('[inert]')).toBeNull(); expect(screen.getByRole('heading').closest('[inert]')).toBeNull(); From c0ddf2c3e12d8e5a62eda8e0b322ad4f2faa4da3 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 25 Sep 2026 10:40:47 -0400 Subject: [PATCH 2/2] test(ui): drop tautological inert assertions --- packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx | 1 - packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx index d3cb709705d..1da263fbd58 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx @@ -1157,7 +1157,6 @@ describe('SignInStart', () => { simulateCaptchaInteractive(getCaptcha()); await waitFor(() => expect(google.closest('[inert]')).not.toBeNull()); - expect(google.closest('[inert]')?.hasAttribute('inert')).toBe(true); // The captcha widget and the header stay outside the inert subtree. expect(getCaptcha().closest('[inert]')).toBeNull(); expect(screen.getByRole('heading').closest('[inert]')).toBeNull(); diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx index cd9a0dca1d6..eb5fb3f4739 100644 --- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx @@ -639,7 +639,6 @@ describe('SignUpStart', () => { simulateCaptchaInteractive(getCaptcha()); await waitFor(() => expect(google.closest('[inert]')).not.toBeNull()); - expect(google.closest('[inert]')?.hasAttribute('inert')).toBe(true); expect(getCaptcha().closest('[inert]')).toBeNull(); expect(screen.getByRole('heading').closest('[inert]')).toBeNull();