Skip to content

Commit

Permalink
Merge pull request #19 from mpontus/feature/specify-container
Browse files Browse the repository at this point in the history
Rename `ModalProvider#contianer` prop to `rootComponent` and reuse it for specifying mount node.
  • Loading branch information
mpontus authored Feb 5, 2020
2 parents 4e410e0 + 38bf196 commit 659e78a
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ import { TransitionGroup } from "react-transition-group";
import App from "./App";

ReactDOM.render(
<ModalProvider container={TransitionGroup}>
<ModalProvider rootComponent={TransitionGroup}>
<App />
</ModalProvider>,
document.getElementById("root")
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-modal-hook",
"version": "2.0.0",
"version": "3.0.0",
"description": "React hook for showing modal windows",
"author": "mpontus",
"license": "MIT",
Expand Down
27 changes: 23 additions & 4 deletions src/ModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { ModalRoot } from "./ModalRoot";
*/
export interface ModalProviderProps {
/**
* Container component for modals that will be passed to ModalRoot
* Specifies the root element to render modals into
*/
container?: React.ComponentType<any>;
container?: Element;

/**
* Container component for modal nodes
*/
rootComponent?: React.ComponentType<any>;

/**
* Subtree that will receive modal context
Expand All @@ -22,7 +27,17 @@ export interface ModalProviderProps {
*
* Provides modal context and renders ModalRoot.
*/
export const ModalProvider = ({ container, children }: ModalProviderProps) => {
export const ModalProvider = ({
container,
rootComponent,
children
}: ModalProviderProps) => {
if (container && !(container instanceof HTMLElement)) {
throw new Error(`Container must specify DOM element to mount modal root into.
This behavior has changed in 3.0.0. Please use \`rootComponent\` prop instead.
See: https://github.com/mpontus/react-modal-hook/issues/18`);
}
const [modals, setModals] = useState<Record<string, ModalType>>({});
const showModal = useCallback(
(key: string, modal: ModalType) =>
Expand All @@ -47,7 +62,11 @@ export const ModalProvider = ({ container, children }: ModalProviderProps) => {
<ModalContext.Provider value={contextValue}>
<React.Fragment>
{children}
<ModalRoot modals={modals} container={container} />
<ModalRoot
modals={modals}
component={rootComponent}
container={container}
/>
</React.Fragment>
</ModalContext.Provider>
);
Expand Down
21 changes: 15 additions & 6 deletions src/ModalRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ interface ModalRootProps {
* used by defualt, specifying a different component can change the way modals
* are rendered across the whole application.
*/
container?: React.ComponentType<any>;
component?: React.ComponentType<any>;

/**
* Specifies the root element to render modals into
*/
container?: Element;
}

/**
Expand Down Expand Up @@ -48,20 +53,24 @@ const ModalRenderer = memo(({ component, ...rest }: ModalRendererProps) =>
* Renders modals using react portal.
*/
export const ModalRoot = memo(
({ modals, container: Container = React.Fragment }: ModalRootProps) => {
({
modals,
container,
component: RootComponent = React.Fragment
}: ModalRootProps) => {
const [mountNode, setMountNode] = useState<Element | undefined>(undefined);

// This effect will not be ran in the server environment
useEffect(() => setMountNode(document.body));
useEffect(() => setMountNode(container || document.body));

return mountNode
? ReactDOM.createPortal(
<Container>
<RootComponent>
{Object.keys(modals).map(key => (
<ModalRenderer key={key} component={modals[key]} />
))}
</Container>,
document.body
</RootComponent>,
mountNode
)
: null;
}
Expand Down
62 changes: 56 additions & 6 deletions src/__tests__/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import React from "react";
import { render, fireEvent, flushEffects } from "react-testing-library";
import {
cleanup,
render,
fireEvent,
flushEffects
} from "react-testing-library";
import { ModalProvider, useModal } from "..";
import "jest-dom/extend-expect";

afterEach(cleanup);

beforeEach(() => {
jest.spyOn(console, "error");
(global.console.error as any).mockImplementation(() => {});
});

afterEach(() => {
(global.console.error as any).mockRestore();
});

describe("custom container prop", () => {
const Container: React.SFC = ({ children }) => (
<div data-testid="custom-container">{children}</div>
const RootComponent: React.SFC = ({ children }) => (
<div data-testid="custom-root">{children}</div>
);

const App = () => {
Expand All @@ -14,18 +30,52 @@ describe("custom container prop", () => {
return <button onClick={showModal}>Show modal</button>;
};

it("should render modals inside custom container", () => {
it("should render modals inside custom root component", () => {
const { getByTestId, getByText } = render(
<ModalProvider container={Container}>
<ModalProvider rootComponent={RootComponent}>
<App />
</ModalProvider>
);

fireEvent.click(getByText("Show modal"));
flushEffects();

expect(getByTestId("custom-container")).toContainElement(
expect(getByTestId("custom-root")).toContainElement(
getByText("This is a modal")
);
});

it("should render modals inside the specified root element", () => {
const customRoot = document.createElement("div");

document.body.appendChild(customRoot);

const { getByText } = render(
<ModalProvider container={customRoot}>
<App />
</ModalProvider>
);

fireEvent.click(getByText("Show modal"));
flushEffects();

expect(customRoot).toContainElement(getByText("This is a modal"));
});

it("should throw an error when `container` does not specify a DOM elemnet", () => {
expect(() => {
render(
<ModalProvider container={React.Fragment as any}>
<App />
</ModalProvider>
);
flushEffects();
}).toThrowError(
expect.objectContaining({
message: expect.stringMatching(
/Container must specify DOM element to mount modal root into/
)
})
);
});
});
1 change: 0 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./ModalContext";
export * from "./ModalProvider";
export * from "./ModalRoot";
export * from "./useModal";

0 comments on commit 659e78a

Please sign in to comment.