Skip to content

Commit 7d4e63c

Browse files
committed
remove PortalRootManager usage
1 parent 998c30a commit 7d4e63c

File tree

3 files changed

+43
-169
lines changed

3 files changed

+43
-169
lines changed
Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use client';
22

3-
import React, { useEffect, useRef } from 'react';
3+
import React from 'react';
44

55
import { createContextAndHook } from './hooks/createContextAndHook';
6-
import { portalRootManager } from './portal-root-manager';
76

87
type PortalProviderProps = React.PropsWithChildren<{
98
/**
@@ -19,9 +18,12 @@ const [PortalContext, , usePortalContextWithoutGuarantee] = createContextAndHook
1918
}>('PortalProvider');
2019

2120
/**
22-
* PortalProvider allows you to specify a custom container for all Clerk floating UI elements
21+
* UNSAFE_PortalProvider allows you to specify a custom container for Clerk floating UI elements
2322
* (popovers, modals, tooltips, etc.) that use portals.
2423
*
24+
* Only components within this provider will be affected. Components outside the provider
25+
* will continue to use the default document.body for portals.
26+
*
2527
* This is particularly useful when using Clerk components inside external UI libraries
2628
* like Radix Dialog or React Aria Components, where portaled elements need to render
2729
* within the dialog's container to remain interactable.
@@ -32,46 +34,32 @@ const [PortalContext, , usePortalContextWithoutGuarantee] = createContextAndHook
3234
* const containerRef = useRef(null);
3335
* return (
3436
* <RadixDialog ref={containerRef}>
35-
* <PortalProvider getContainer={() => containerRef.current}>
37+
* <UNSAFE_PortalProvider getContainer={() => containerRef.current}>
3638
* <UserButton />
37-
* </PortalProvider>
39+
* </UNSAFE_PortalProvider>
3840
* </RadixDialog>
3941
* );
4042
* }
4143
* ```
4244
*/
4345
export const UNSAFE_PortalProvider = ({ children, getContainer }: PortalProviderProps) => {
44-
const getContainerRef = useRef(getContainer);
45-
getContainerRef.current = getContainer;
46-
47-
// Register with the manager for cross-tree access (e.g., modals in Components.tsx)
48-
useEffect(() => {
49-
const getContainerWrapper = () => getContainerRef.current();
50-
portalRootManager.push(getContainerWrapper);
51-
return () => {
52-
portalRootManager.pop();
53-
};
54-
}, []);
55-
56-
// Provide context for same-tree access (e.g., UserButton popover)
5746
const contextValue = React.useMemo(() => ({ value: { getContainer } }), [getContainer]);
5847

5948
return <PortalContext.Provider value={contextValue}>{children}</PortalContext.Provider>;
6049
};
6150

6251
/**
6352
* Hook to get the current portal root container.
64-
* First checks React context (for same-tree components),
65-
* then falls back to PortalRootManager (for cross-tree like modals).
53+
* Returns the getContainer function from context if inside a PortalProvider,
54+
* otherwise returns a function that returns null (default behavior).
6655
*/
6756
export const usePortalRoot = (): (() => HTMLElement | null) => {
68-
// Try to get from context first (for components in the same React tree)
6957
const contextValue = usePortalContextWithoutGuarantee();
7058

7159
if (contextValue && 'getContainer' in contextValue && contextValue.getContainer) {
7260
return contextValue.getContainer;
7361
}
7462

75-
// Fall back to manager (for components in different React trees, like modals)
76-
return portalRootManager.getCurrent.bind(portalRootManager);
63+
// Return a function that returns null when not inside a PortalProvider
64+
return () => null;
7765
};
Lines changed: 32 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { describe, expect, it } from 'vitest';
22
import React from 'react';
33
import { render, screen } from '@testing-library/react';
44

55
import { UNSAFE_PortalProvider, usePortalRoot } from '../PortalProvider';
6-
import { portalRootManager } from '../portal-root-manager';
76

8-
describe('PortalProvider', () => {
7+
describe('UNSAFE_PortalProvider', () => {
98
it('provides getContainer to children via context', () => {
109
const container = document.createElement('div');
1110
const getContainer = () => container;
@@ -24,38 +23,31 @@ describe('PortalProvider', () => {
2423
expect(screen.getByTestId('test').textContent).toBe('found');
2524
});
2625

27-
it('registers with portalRootManager on mount', () => {
26+
it('only affects components within the provider', () => {
2827
const container = document.createElement('div');
2928
const getContainer = () => container;
30-
const pushSpy = vi.spyOn(portalRootManager, 'push');
3129

32-
const { unmount } = render(
33-
<UNSAFE_PortalProvider getContainer={getContainer}>
34-
<div>test</div>
35-
</UNSAFE_PortalProvider>,
36-
);
37-
38-
expect(pushSpy).toHaveBeenCalledTimes(1);
39-
expect(portalRootManager.getCurrent()).toBe(container);
40-
41-
unmount();
42-
});
30+
const InsideComponent = () => {
31+
const portalRoot = usePortalRoot();
32+
return <div data-testid='inside'>{portalRoot() === container ? 'container' : 'null'}</div>;
33+
};
4334

44-
it('unregisters from portalRootManager on unmount', () => {
45-
const container = document.createElement('div');
46-
const getContainer = () => container;
47-
const popSpy = vi.spyOn(portalRootManager, 'pop');
35+
const OutsideComponent = () => {
36+
const portalRoot = usePortalRoot();
37+
return <div data-testid='outside'>{portalRoot() === null ? 'null' : 'container'}</div>;
38+
};
4839

49-
const { unmount } = render(
50-
<UNSAFE_PortalProvider getContainer={getContainer}>
51-
<div>test</div>
52-
</UNSAFE_PortalProvider>,
40+
render(
41+
<>
42+
<OutsideComponent />
43+
<UNSAFE_PortalProvider getContainer={getContainer}>
44+
<InsideComponent />
45+
</UNSAFE_PortalProvider>
46+
</>,
5347
);
5448

55-
unmount();
56-
57-
expect(popSpy).toHaveBeenCalledTimes(1);
58-
expect(portalRootManager.getCurrent()).toBeNull();
49+
expect(screen.getByTestId('inside').textContent).toBe('container');
50+
expect(screen.getByTestId('outside').textContent).toBe('null');
5951
});
6052
});
6153

@@ -78,103 +70,34 @@ describe('usePortalRoot', () => {
7870
expect(screen.getByTestId('test').textContent).toBe('found');
7971
});
8072

81-
it('returns manager.getCurrent when outside PortalProvider', () => {
82-
const container = document.createElement('div');
83-
portalRootManager.push(() => container);
84-
73+
it('returns a function that returns null when outside PortalProvider', () => {
8574
const TestComponent = () => {
8675
const portalRoot = usePortalRoot();
87-
return <div data-testid='test'>{portalRoot() === container ? 'found' : 'not-found'}</div>;
76+
return <div data-testid='test'>{portalRoot() === null ? 'null' : 'not-null'}</div>;
8877
};
8978

9079
render(<TestComponent />);
9180

92-
expect(screen.getByTestId('test').textContent).toBe('found');
93-
94-
portalRootManager.pop();
81+
expect(screen.getByTestId('test').textContent).toBe('null');
9582
});
9683

97-
it('context value takes precedence over manager', () => {
98-
const contextContainer = document.createElement('div');
99-
const managerContainer = document.createElement('div');
100-
const contextGetContainer = () => contextContainer;
101-
102-
portalRootManager.push(() => managerContainer);
84+
it('supports nested providers with innermost taking precedence', () => {
85+
const outerContainer = document.createElement('div');
86+
const innerContainer = document.createElement('div');
10387

10488
const TestComponent = () => {
10589
const portalRoot = usePortalRoot();
106-
return <div data-testid='test'>{portalRoot() === contextContainer ? 'found' : 'not-found'}</div>;
90+
return <div data-testid='test'>{portalRoot() === innerContainer ? 'inner' : 'outer'}</div>;
10791
};
10892

10993
render(
110-
<UNSAFE_PortalProvider getContainer={contextGetContainer}>
111-
<TestComponent />
94+
<UNSAFE_PortalProvider getContainer={() => outerContainer}>
95+
<UNSAFE_PortalProvider getContainer={() => innerContainer}>
96+
<TestComponent />
97+
</UNSAFE_PortalProvider>
11298
</UNSAFE_PortalProvider>,
11399
);
114100

115-
expect(screen.getByTestId('test').textContent).toBe('found');
116-
117-
portalRootManager.pop();
118-
});
119-
});
120-
121-
describe('portalRootManager', () => {
122-
beforeEach(() => {
123-
// Clear the stack before each test
124-
while (portalRootManager.getCurrent() !== null) {
125-
portalRootManager.pop();
126-
}
127-
});
128-
129-
it('maintains stack of portal roots', () => {
130-
const container1 = document.createElement('div');
131-
const container2 = document.createElement('div');
132-
const getContainer1 = () => container1;
133-
const getContainer2 = () => container2;
134-
135-
portalRootManager.push(getContainer1);
136-
portalRootManager.push(getContainer2);
137-
138-
expect(portalRootManager.getCurrent()).toBe(container2);
139-
140-
portalRootManager.pop();
141-
expect(portalRootManager.getCurrent()).toBe(container1);
142-
143-
portalRootManager.pop();
144-
});
145-
146-
it('getCurrent returns topmost root', () => {
147-
const container1 = document.createElement('div');
148-
const container2 = document.createElement('div');
149-
const getContainer1 = () => container1;
150-
const getContainer2 = () => container2;
151-
152-
portalRootManager.push(getContainer1);
153-
portalRootManager.push(getContainer2);
154-
155-
expect(portalRootManager.getCurrent()).toBe(container2);
156-
157-
portalRootManager.pop();
158-
portalRootManager.pop();
159-
});
160-
161-
it('pop removes topmost root', () => {
162-
const container1 = document.createElement('div');
163-
const container2 = document.createElement('div');
164-
const getContainer1 = () => container1;
165-
const getContainer2 = () => container2;
166-
167-
portalRootManager.push(getContainer1);
168-
portalRootManager.push(getContainer2);
169-
170-
portalRootManager.pop();
171-
172-
expect(portalRootManager.getCurrent()).toBe(container1);
173-
174-
portalRootManager.pop();
175-
});
176-
177-
it('getCurrent returns null when stack is empty', () => {
178-
expect(portalRootManager.getCurrent()).toBeNull();
101+
expect(screen.getByTestId('test').textContent).toBe('inner');
179102
});
180103
});

packages/shared/src/react/portal-root-manager.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)