Skip to content

Commit 659e78a

Browse files
authored
Merge pull request #19 from mpontus/feature/specify-container
Rename `ModalProvider#contianer` prop to `rootComponent` and reuse it for specifying mount node.
2 parents 4e410e0 + 38bf196 commit 659e78a

6 files changed

Lines changed: 96 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ import { TransitionGroup } from "react-transition-group";
116116
import App from "./App";
117117

118118
ReactDOM.render(
119-
<ModalProvider container={TransitionGroup}>
119+
<ModalProvider rootComponent={TransitionGroup}>
120120
<App />
121121
</ModalProvider>,
122122
document.getElementById("root")

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-modal-hook",
3-
"version": "2.0.0",
3+
"version": "3.0.0",
44
"description": "React hook for showing modal windows",
55
"author": "mpontus",
66
"license": "MIT",

src/ModalProvider.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import { ModalRoot } from "./ModalRoot";
77
*/
88
export interface ModalProviderProps {
99
/**
10-
* Container component for modals that will be passed to ModalRoot
10+
* Specifies the root element to render modals into
1111
*/
12-
container?: React.ComponentType<any>;
12+
container?: Element;
13+
14+
/**
15+
* Container component for modal nodes
16+
*/
17+
rootComponent?: React.ComponentType<any>;
1318

1419
/**
1520
* Subtree that will receive modal context
@@ -22,7 +27,17 @@ export interface ModalProviderProps {
2227
*
2328
* Provides modal context and renders ModalRoot.
2429
*/
25-
export const ModalProvider = ({ container, children }: ModalProviderProps) => {
30+
export const ModalProvider = ({
31+
container,
32+
rootComponent,
33+
children
34+
}: ModalProviderProps) => {
35+
if (container && !(container instanceof HTMLElement)) {
36+
throw new Error(`Container must specify DOM element to mount modal root into.
37+
38+
This behavior has changed in 3.0.0. Please use \`rootComponent\` prop instead.
39+
See: https://github.com/mpontus/react-modal-hook/issues/18`);
40+
}
2641
const [modals, setModals] = useState<Record<string, ModalType>>({});
2742
const showModal = useCallback(
2843
(key: string, modal: ModalType) =>
@@ -47,7 +62,11 @@ export const ModalProvider = ({ container, children }: ModalProviderProps) => {
4762
<ModalContext.Provider value={contextValue}>
4863
<React.Fragment>
4964
{children}
50-
<ModalRoot modals={modals} container={container} />
65+
<ModalRoot
66+
modals={modals}
67+
component={rootComponent}
68+
container={container}
69+
/>
5170
</React.Fragment>
5271
</ModalContext.Provider>
5372
);

src/ModalRoot.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ interface ModalRootProps {
1818
* used by defualt, specifying a different component can change the way modals
1919
* are rendered across the whole application.
2020
*/
21-
container?: React.ComponentType<any>;
21+
component?: React.ComponentType<any>;
22+
23+
/**
24+
* Specifies the root element to render modals into
25+
*/
26+
container?: Element;
2227
}
2328

2429
/**
@@ -48,20 +53,24 @@ const ModalRenderer = memo(({ component, ...rest }: ModalRendererProps) =>
4853
* Renders modals using react portal.
4954
*/
5055
export const ModalRoot = memo(
51-
({ modals, container: Container = React.Fragment }: ModalRootProps) => {
56+
({
57+
modals,
58+
container,
59+
component: RootComponent = React.Fragment
60+
}: ModalRootProps) => {
5261
const [mountNode, setMountNode] = useState<Element | undefined>(undefined);
5362

5463
// This effect will not be ran in the server environment
55-
useEffect(() => setMountNode(document.body));
64+
useEffect(() => setMountNode(container || document.body));
5665

5766
return mountNode
5867
? ReactDOM.createPortal(
59-
<Container>
68+
<RootComponent>
6069
{Object.keys(modals).map(key => (
6170
<ModalRenderer key={key} component={modals[key]} />
6271
))}
63-
</Container>,
64-
document.body
72+
</RootComponent>,
73+
mountNode
6574
)
6675
: null;
6776
}

src/__tests__/ModalProvider.tsx

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import React from "react";
2-
import { render, fireEvent, flushEffects } from "react-testing-library";
2+
import {
3+
cleanup,
4+
render,
5+
fireEvent,
6+
flushEffects
7+
} from "react-testing-library";
38
import { ModalProvider, useModal } from "..";
49
import "jest-dom/extend-expect";
510

11+
afterEach(cleanup);
12+
13+
beforeEach(() => {
14+
jest.spyOn(console, "error");
15+
(global.console.error as any).mockImplementation(() => {});
16+
});
17+
18+
afterEach(() => {
19+
(global.console.error as any).mockRestore();
20+
});
21+
622
describe("custom container prop", () => {
7-
const Container: React.SFC = ({ children }) => (
8-
<div data-testid="custom-container">{children}</div>
23+
const RootComponent: React.SFC = ({ children }) => (
24+
<div data-testid="custom-root">{children}</div>
925
);
1026

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

17-
it("should render modals inside custom container", () => {
33+
it("should render modals inside custom root component", () => {
1834
const { getByTestId, getByText } = render(
19-
<ModalProvider container={Container}>
35+
<ModalProvider rootComponent={RootComponent}>
2036
<App />
2137
</ModalProvider>
2238
);
2339

2440
fireEvent.click(getByText("Show modal"));
2541
flushEffects();
2642

27-
expect(getByTestId("custom-container")).toContainElement(
43+
expect(getByTestId("custom-root")).toContainElement(
2844
getByText("This is a modal")
2945
);
3046
});
47+
48+
it("should render modals inside the specified root element", () => {
49+
const customRoot = document.createElement("div");
50+
51+
document.body.appendChild(customRoot);
52+
53+
const { getByText } = render(
54+
<ModalProvider container={customRoot}>
55+
<App />
56+
</ModalProvider>
57+
);
58+
59+
fireEvent.click(getByText("Show modal"));
60+
flushEffects();
61+
62+
expect(customRoot).toContainElement(getByText("This is a modal"));
63+
});
64+
65+
it("should throw an error when `container` does not specify a DOM elemnet", () => {
66+
expect(() => {
67+
render(
68+
<ModalProvider container={React.Fragment as any}>
69+
<App />
70+
</ModalProvider>
71+
);
72+
flushEffects();
73+
}).toThrowError(
74+
expect.objectContaining({
75+
message: expect.stringMatching(
76+
/Container must specify DOM element to mount modal root into/
77+
)
78+
})
79+
);
80+
});
3181
});

src/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from "./ModalContext";
22
export * from "./ModalProvider";
3-
export * from "./ModalRoot";
43
export * from "./useModal";

0 commit comments

Comments
 (0)