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
12 changes: 3 additions & 9 deletions src/core/primitives/Dialog/fragments/DialogPrimitivePortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,16 @@ const DialogPrimitivePortal = ({
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
// Only run on client side after component mounts
if (container) {
rootElementRef.current = container as HTMLElement;
} else {
const themeContainer = themeContext?.portalRootRef.current
|| document.querySelector('[data-rad-ui-portal-root]') as HTMLElement | null
|| themeContext?.containerRef.current
|| document.querySelector('#rad-ui-theme-container') as HTMLElement | null;
const fallback = document.body;
const selectedRoot = themeContainer || fallback;
rootElementRef.current = selectedRoot;
rootElementRef.current = themeContext?.portalRootRef.current
?? themeContext?.containerRef.current
?? document.body;
}
setIsMounted(true);
}, [container, themeContext]);

// Don't render anything until mounted (SSR safety)
if (!isMounted) {
return null;
}
Expand Down
28 changes: 28 additions & 0 deletions src/core/primitives/Dialog/tests/DialogPrimitive.portal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ThemeContext from '~/components/ui/Theme/ThemeContext';
import DialogPrimitive from '~/core/primitives/Dialog';

describe('Dialog primitive portal', () => {
test('portals into Theme portalRootRef when provided', () => {
const portalRoot = document.createElement('div');
document.body.appendChild(portalRoot);

render(
<ThemeContext.Provider
value={{
containerRef: { current: null },
portalRootRef: { current: portalRoot }
}}>
<DialogPrimitive.Root open>
<DialogPrimitive.Portal>
<DialogPrimitive.Content>Dialog body</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
</ThemeContext.Provider>
);

expect(portalRoot).toContainElement(screen.getByText('Dialog body'));
portalRoot.remove();
});
});
25 changes: 22 additions & 3 deletions src/test-utils/portal.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ThemeContext from '~/components/ui/Theme/ThemeContext';

export function renderWithPortal(ui: React.ReactElement) {
const portalRoot = document.createElement('div');
portalRoot.setAttribute('id', 'rad-ui-theme-container');
portalRoot.setAttribute('data-rad-ui-portal-root', '');
document.body.appendChild(portalRoot);
const result = render(ui);

const themeContainer = document.createElement('div');
themeContainer.setAttribute('id', 'rad-ui-theme-container');
document.body.appendChild(themeContainer);

const result = render(
React.createElement(
ThemeContext.Provider,
{
value: {
containerRef: { current: themeContainer },
portalRootRef: { current: portalRoot }
}
},
ui
)
);

return {
...result,
portalRoot,
cleanup: () => {
result.unmount();
portalRoot.remove();
themeContainer.remove();
}
};
}
Expand All @@ -28,7 +48,6 @@ export async function assertFocusTrap(container: HTMLElement) {
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
// TODO: track jsdom focus trap flakiness and re-enable assertions in CI when stable.
const isJsdom = typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent);

first.focus();
Expand Down
Loading