@@ -131,4 +150,416 @@ describe('ReactDOMActivity', () => {
);
},
);
+
+ // @gate enableActivity
+ it('hides new portals added to an already hidden tree', async () => {
+ function Child() {
+ return
;
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Mount hidden tree.
+ await act(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ assertLog(['Parent']);
+ expect(container.innerHTML).toBe(
+ '
',
+ );
+ expect(portalContainer.innerHTML).toBe('');
+
+ // Add a portal inside the hidden tree.
+ await act(() => {
+ root.render(
+
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['Parent', 'Child']);
+ expect(container.innerHTML).toBe(
+ '
',
+ );
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+
+ // Now reveal it.
+ await act(() => {
+ root.render(
+
+
+
+
+
+ ,
+ );
+ });
+
+ assertLog(['Parent', 'Child']);
+ expect(container.innerHTML).toBe(
+ '
',
+ );
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+ });
+
+ // @gate enableActivity
+ it('hides new insertions inside an already hidden portal', async () => {
+ function Child({text}) {
+ useLayoutEffect(() => {
+ Scheduler.log(`Mount layout ${text}`);
+ return () => {
+ Scheduler.log(`Unmount layout ${text}`);
+ };
+ }, [text]);
+ return
;
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Mount hidden tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['A']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+
+ // Add a node inside the hidden portal.
+ await act(() => {
+ root.render(
+
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['A', 'B']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+
+ // Now reveal it.
+ await act(() => {
+ root.render(
+
+
+
+
+
+ ,
+ );
+ });
+
+ assertLog(['A', 'B', 'Mount layout A', 'Mount layout B']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+ });
+
+ // @gate enableActivity
+ it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => {
+ const promise = new Promise(() => {});
+
+ function Child({showInner}) {
+ useLayoutEffect(() => {
+ Scheduler.log('Mount layout');
+ return () => {
+ Scheduler.log('Unmount layout');
+ };
+ }, []);
+ return (
+ <>
+ {showInner ? null : promise}
+
+ >
+ );
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+
+ // Prerender the whole tree.
+ await act(() => {
+ root.render(
+
+
+ Loading}>
+
+
+
+ ,
+ );
+ });
+
+ assertLog(['Child']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+
+ // Re-suspend the inner.
+ await act(() => {
+ root.render(
+
+
+ Loading}>
+
+
+
+ ,
+ );
+ });
+ assertLog([]);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
Loading',
+ );
+
+ // Toggle to visible while suspended.
+ await act(() => {
+ root.render(
+
+
+ Loading}>
+
+
+
+ ,
+ );
+ });
+ assertLog([]);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
Loading',
+ );
+
+ // Now reveal.
+ await act(() => {
+ root.render(
+
+
+ Loading}>
+
+
+
+ ,
+ );
+ });
+ assertLog(['Child', 'Mount layout']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+ });
+
+ // @gate enableActivity
+ it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => {
+ function Child() {
+ useLayoutEffect(() => {
+ Scheduler.log('Mount layout');
+ return () => {
+ Scheduler.log('Unmount layout');
+ };
+ }, []);
+ return
;
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Mount visible tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['Child', 'Mount layout']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe('
');
+
+ // Hide the tree. The layout effect is unmounted.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['Unmount layout', 'Child']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+ });
+
+ // @gate enableActivity
+ it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => {
+ function Child() {
+ useLayoutEffect(() => {
+ Scheduler.log('Mount layout');
+ return () => {
+ Scheduler.log('Unmount layout');
+ };
+ }, []);
+ return
;
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Mount hidden tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ // No layout effect.
+ assertLog(['Child']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+
+ // Unhide the tree. The layout effect is mounted.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['Child', 'Mount layout']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe(
+ '
',
+ );
+ });
+
+ // @gate enableLegacyHidden
+ it('does not toggle effects or hide nodes for LegacyHidden component inside portal', async () => {
+ function Child() {
+ useLayoutEffect(() => {
+ Scheduler.log('Mount layout');
+ return () => {
+ Scheduler.log('Unmount layout');
+ };
+ }, []);
+ useEffect(() => {
+ Scheduler.log('Mount passive');
+ return () => {
+ Scheduler.log('Unmount passive');
+ };
+ }, []);
+ return
;
+ }
+
+ const portalContainer = document.createElement('div');
+
+ function Portal({children}) {
+ return
{ReactDOM.createPortal(children, portalContainer)}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Mount visible tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ assertLog(['Child', 'Mount layout', 'Mount passive']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe('
');
+
+ // Hide the tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ // Effects not unmounted.
+ assertLog(['Child']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe('
');
+
+ // Unhide the tree.
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ // Effects already mounted.
+ assertLog(['Child']);
+ expect(container.innerHTML).toBe('
');
+ expect(portalContainer.innerHTML).toBe('
');
+ });
});