Skip to content
Open
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/fix-inert-react-19-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/ui": patch
---

Fix `inert` attribute to work correctly in React 19. The previous value `''` (empty string) is falsy and not set by React 19's boolean attribute handler, leaving hidden panel and collapsible content interactive. Switches to `'true'` which is truthy in both React 18 and 19.
13 changes: 13 additions & 0 deletions packages/headless/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// `export {}` makes this file a module so `declare module 'react'` augments React's
// types instead of replacing them (an ambient declaration would shadow React, breaking
// type resolution package-wide).
export {};

declare module 'react' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLAttributes<T> {
// `inert` landed in @types/react v19; augment for React 18 compatibility.
// Use 'true' (truthy string) — '' is falsy in React 19 and won't set the attribute.
inert?: 'true' | undefined;
}
}
23 changes: 23 additions & 0 deletions packages/headless/src/primitives/tabs/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,29 @@ describe('Tabs', () => {
const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"][data-cl-hidden]');
expect(panels).toHaveLength(2);
});

it('non-selected panels have inert attribute, selected panel does not', () => {
renderTabs();
const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"]');
const inert = Array.from(panels).filter(p => p.hasAttribute('inert'));
const notInert = Array.from(panels).filter(p => !p.hasAttribute('inert'));
// Presence check only — React 18 renders inert="true", React 19 normalises to inert=""
expect(inert).toHaveLength(2);
expect(notInert).toHaveLength(1);
});

it('inert updates when selection changes', async () => {
const user = userEvent.setup();
renderTabs();

await user.click(screen.getByText('Settings'));

const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"]');
const [account, settings, billing] = Array.from(panels);
expect(account).toHaveAttribute('inert');
expect(settings).not.toHaveAttribute('inert');
expect(billing).toHaveAttribute('inert');
});
});

describe('roving tabindex', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ export function PricingTableMatrix({
}),
feePeriodNoticeAnimation,
]}
// @ts-ignore - Needed until React 19 support
inert={planPeriod !== 'annual' ? 'true' : undefined}
>
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ function KeylessPromptInternal(props: KeylessPromptProps) {
</button>
<div
id={id}
{...(!isOpen && { inert: '' as any })}
{...(!isOpen && { inert: 'true' })}
css={css`
${CSS_RESET};
display: grid;
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/src/elements/Collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ export function Collapsible({ open, children, sx }: CollapsibleProps): JSX.Eleme
}),
sx,
]}
// @ts-ignore - inert not yet in React types
inert={!open ? '' : undefined}
inert={!open ? 'true' : undefined}
>
<Box
elementDescriptor={descriptors.collapsibleInner}
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/elements/__tests__/Collapsible.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,16 @@ describe('Collapsible', () => {
});

describe('Inert Attribute', () => {
it('sets inert to empty string when open={false}', async () => {
it('sets inert when open={false}', async () => {
const { wrapper } = await createFixtures();
const { container, rerender } = render(<Collapsible open>Content</Collapsible>, { wrapper });

await waitForAnimationFrame();
rerender(<Collapsible open={false}>Content</Collapsible>);

const element = container.querySelector('.cl-collapsible') as HTMLElement;
expect(element).toHaveAttribute('inert', '');
// Check presence only — React 18 renders inert="true", React 19 normalises to inert=""
expect(element).toHaveAttribute('inert');
});

it('does not set inert when open={true}', async () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { Clerk } from '@clerk/shared/types';
import type { ClerkUIConstructor } from '@clerk/shared/ui';

declare module 'react' {
interface HTMLAttributes<T> {
// `inert` landed in @types/react v19; augment for React 18 compatibility.
// Use 'true' (truthy string) — '' is falsy in React 19 and won't set the attribute.
inert?: 'true' | undefined;
}
}

declare module '*.svg' {
const value: React.FC<React.SVGAttributes<SVGElement>>;
export default value;
Expand Down
Loading