Skip to content

Commit

Permalink
feat: auto delete portals on container removal
Browse files Browse the repository at this point in the history
This allows for decorations widgets to use portals.
  • Loading branch information
ifiokjr committed Jul 31, 2019
1 parent bdf2f82 commit cfdb047
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 9 deletions.
62 changes: 57 additions & 5 deletions @remirror/react/src/components/__tests__/remirror-portals.spec.tsx
@@ -1,19 +1,71 @@
import { PortalContainer } from '@remirror/core';
import { act, render } from '@testing-library/react';
import { useRemirrorManager } from '../../hooks';
import { RemirrorManager } from '../remirror-manager';
import { RemirrorPortals } from '../remirror-portals';

describe('NodeViewPortalComponent', () => {
describe('RemirrorPortals', () => {
const container = document.createElement('span');
container.id = 'root';

afterEach(() => {
document.body.innerHTML = '';
});

it('should update on render', () => {
const portalContainer = new PortalContainer();
const { getByTestId } = render(<RemirrorPortals portalContainer={portalContainer} />);
const mockRender = jest.fn(() => <div data-testid='test' />);

act(() => {
const mockRender = jest.fn(() => <div data-testid='test' />);
const element = document.createElement('span');
document.body.appendChild(element);
portalContainer.render({ render: mockRender, container: element });
document.body.appendChild(container);
portalContainer.render({ render: mockRender, container });
});

expect(getByTestId('test')).toBeTruthy();
expect(mockRender).toBeCalledWith({}, {});
});

it('provides access to the manager context', () => {
expect.assertions(1);
const Component = () => {
const manager = useRemirrorManager();

expect(manager).toBeTruthy();
return <div data-testid='test' />;
};

const portalContainer = new PortalContainer();
render(
<RemirrorManager>
<RemirrorPortals portalContainer={portalContainer} />
</RemirrorManager>,
);

act(() => {
document.body.appendChild(container);
portalContainer.render({ render: Component, container });
});
});

it('removes the portal the when dom node is removed', () => {
expect.assertions(3);

const portalContainer = new PortalContainer();
const { queryByTestId } = render(<RemirrorPortals portalContainer={portalContainer} />);

act(() => {
document.body.appendChild(container);
portalContainer.render({ render: () => <div data-testid='test' />, container });
});

expect(queryByTestId('test')).toBeTruthy();

document.body.removeChild(container);
portalContainer.on(portals => {
expect(portals.has(container)).toBeFalse();
});

expect(queryByTestId('test')).toBeNull();
});
});
46 changes: 42 additions & 4 deletions @remirror/react/src/components/remirror-portals.tsx
@@ -1,4 +1,4 @@
import React, { Fragment, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

import { PortalContainer, PortalMap } from '@remirror/core';
Expand All @@ -12,6 +12,9 @@ export interface RemirrorPortalsProps {

/**
* The component that places all the portals into the DOM.
*
* Portals can be created by NodeView and also the static
* widget method on Decorations.
*/
export const RemirrorPortals = ({ portalContainer }: RemirrorPortalsProps) => {
const [state, setState] = useState(Array.from(portalContainer.portals.entries()));
Expand All @@ -30,9 +33,44 @@ export const RemirrorPortals = ({ portalContainer }: RemirrorPortalsProps) => {

return (
<>
{state.map(([container, { render: Component, key }]) => (
<Fragment key={key}>{createPortal(<Component />, container)}</Fragment>
))}
{state.map(([container, { render: Component, key }]) =>
createPortal(
<Portal container={container} Component={Component} portalContainer={portalContainer} />,
container,
key,
),
)}
</>
);
};

export interface PortalProps extends RemirrorPortalsProps {
/**
* Holds the element that this portal is being rendered into.
*/
container: HTMLElement;

/**
* The plain component to render.
*/
Component: () => JSX.Element;
}

/**
* This is the component rendered by the createPortal method within the
* RemirrorPortals component. It is responsible for cleanup when the container
* is removed from the DOM.
*/
const Portal = ({ portalContainer, container, Component }: PortalProps) => {
useEffect(() => {
/**
* Remove the portal container entry when this portal is unmounted.
* Portals are unmounted when their host container is removed from the dom.
*/
return () => {
portalContainer.remove(container);
};
}, [portalContainer]);

return <Component />;
};

0 comments on commit cfdb047

Please sign in to comment.