Skip to content

Commit

Permalink
Implement support for multiple modals
Browse files Browse the repository at this point in the history
  • Loading branch information
mpontus committed Nov 29, 2018
1 parent d53aab1 commit c254975
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 47 deletions.
8 changes: 4 additions & 4 deletions src/ModalContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ export type ModalType = React.ComponentType<any>;
* Shape of the modal context
*/
export interface ModalContextType {
modal: ModalType | undefined;
showModal(component: ModalType): void;
hideModal(): void;
modals: Record<string, ModalType>;
showModal(key: string, component: ModalType): void;
hideModal(key: string): void;
}

/**
* Modal Context Object
*/
export const ModalContext = React.createContext<ModalContextType>({
modal: undefined,
modals: {},
showModal: noop,
hideModal: noop
});
35 changes: 26 additions & 9 deletions src/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useCallback, useState, useMemo } from "react";
import { ModalType, ModalContext } from "./ModalContext";
import { ModalRoot } from "./ModalRoot";

Expand All @@ -18,16 +18,33 @@ export interface ModalProviderProps {
* Provides Modal Context to children.
*/
export const ModalProvider = ({ children }: ModalProviderProps) => {
const [modal, setModal] = useState<ModalType | undefined>(undefined);
const [modals, setModals] = useState<Record<string, ModalType>>({});
const showModal = useCallback(
(key: string, modal: ModalType) =>
setModals(modals => ({
...modals,
[key]: modal
})),
[setModals]
);
const hideModal = useCallback(
(key: string) =>
setModals(modals => {
const newModals = { ...modals };
delete newModals[key];
return newModals;
}),
[setModals]
);

const contextValue = useMemo(() => ({ modals, showModal, hideModal }), [
modals,
showModal,
hideModal
]);

return (
<ModalContext.Provider
value={{
modal,
showModal: modal => setModal(() => modal),
hideModal: () => setModal(undefined)
}}
>
<ModalContext.Provider value={contextValue}>
<React.Fragment>
{children}
<ModalRoot />
Expand Down
15 changes: 10 additions & 5 deletions src/ModalRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { ModalContext } from "./ModalContext";
* Renders modals using a portal.
*/
export const ModalRoot = () => {
const { modal: Component } = useContext(ModalContext);
const { modals } = useContext(ModalContext);

if (Component === undefined) {
return null;
}
return ReactDOM.createPortal(
<React.Fragment>
{Object.keys(modals).map(key => {
const Component = modals[key];

return ReactDOM.createPortal(<Component />, document.body);
return <Component key={key} />;
})}
</React.Fragment>,
document.body
);
};
78 changes: 62 additions & 16 deletions src/__tests__/useModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,77 @@ import { useModal } from "../useModal";
const renderWithProvider = (content: React.ReactNode) =>
render(<ModalProvider>{content}</ModalProvider>);

// Test component which calls useModal
const Component = () => {
const [showModal, hideModal] = useModal(() => <div>Modal content</div>);
it("should work with single modal", () => {
const Component = () => {
const [showModal, hideModal] = useModal(() => <div>Modal content</div>);

return (
<React.Fragment>
<button onClick={showModal}>Show modal</button>
<button onClick={hideModal}>Hide modal</button>
</React.Fragment>
);
};
return (
<React.Fragment>
<button onClick={showModal}>Show modal</button>
<button onClick={hideModal}>Hide modal</button>
</React.Fragment>
);
};

test("showModal works", () => {
const { getByText } = renderWithProvider(<Component />);
const { getByText, queryByText } = renderWithProvider(<Component />);

expect(queryByText("Modal content")).not.toBeTruthy();

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

expect(getByText("Modal content")).toBeTruthy();

fireEvent.click(getByText("Hide modal"));

expect(queryByText("Modal content")).not.toBeTruthy();
});

test("hideModal works", () => {
it("should work with multiple modals", () => {
const Component = () => {
const [showFirstModal, hideFirstModal] = useModal(() => (
<div>
<span>First modal</span>
</div>
));

const [showSecondModal, hideSecondModal] = useModal(() => (
<div>
<span>Second modal</span>
</div>
));

return (
<React.Fragment>
<button onClick={hideFirstModal}>Hide first modal</button>
<button onClick={showFirstModal}>Show first modal</button>
<button onClick={showSecondModal}>Show second modal</button>
<button onClick={hideSecondModal}>Hide second modal</button>
</React.Fragment>
);
};

const { getByText, queryByText } = renderWithProvider(<Component />);

fireEvent.click(getByText("Show modal"));
fireEvent.click(getByText("Hide modal"));
expect(queryByText("First modal")).not.toBeTruthy();
expect(queryByText("Second modal")).not.toBeTruthy();

expect(queryByText("Modal content")).not.toBeTruthy();
fireEvent.click(getByText("Show first modal"));

expect(getByText("First modal")).toBeTruthy();
expect(queryByText("Second modal")).not.toBeTruthy();

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

expect(getByText("First modal")).toBeTruthy();
expect(getByText("Second modal")).toBeTruthy();

fireEvent.click(getByText("Hide first modal"));

expect(queryByText("First modal")).not.toBeTruthy();
expect(getByText("Second modal")).toBeTruthy();

fireEvent.click(getByText("Hide second modal"));

expect(queryByText("First modal")).not.toBeTruthy();
expect(queryByText("Second modal")).not.toBeTruthy();
});
10 changes: 10 additions & 0 deletions src/useGlobalId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useMemo } from "react";

/**
* React hook to generate unique id per component instance.
*/
export const useGlobalId = (() => {
let counter = 0;

return () => useMemo(() => `${++counter}`, []);
})();
4 changes: 3 additions & 1 deletion src/useModal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext } from "react";
import { ModalType, ModalContext } from "./ModalContext";
import { useGlobalId } from "./useGlobalId";

/**
* Callback for showing the modal
Expand All @@ -15,7 +16,8 @@ type HideModal = () => void;
* React hook for showing modal windows
*/
export const useModal = (modal: ModalType): [ShowModal, HideModal] => {
const key = useGlobalId();
const { showModal, hideModal } = useContext(ModalContext);

return [() => showModal(modal), () => hideModal()];
return [() => showModal(key, modal), () => hideModal(key)];
};
17 changes: 5 additions & 12 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6628,14 +6628,14 @@ react-dev-utils@^5.0.1:
strip-ansi "3.0.1"
text-table "0.2.0"

[email protected].0:
version "16.7.0-alpha.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0-alpha.0.tgz#8379158d4c76d63c989f325f45dfa5762582584f"
[email protected].2:
version "16.7.0-alpha.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0-alpha.2.tgz#16632880ed43676315991d8b412cce6975a30282"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.11.0-alpha.0"
scheduler "^0.12.0-alpha.2"

react-error-overlay@^4.0.1:
version "4.0.1"
Expand Down Expand Up @@ -6691,7 +6691,7 @@ react-testing-library@^5.3.1:
dependencies:
dom-testing-library "^3.12.0"

react@^16.7.0-alpha.0:
react@^16.7.0-alpha.2:
version "16.7.0-alpha.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.7.0-alpha.2.tgz#924f2ae843a46ea82d104a8def7a599fbf2c78ce"
dependencies:
Expand Down Expand Up @@ -7204,13 +7204,6 @@ saxes@^3.1.3:
dependencies:
xmlchars "^1.3.1"

scheduler@^0.11.0-alpha.0:
version "0.11.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.2.tgz#a8db5399d06eba5abac51b705b7151d2319d33d3"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"

scheduler@^0.12.0-alpha.2:
version "0.12.0-alpha.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0-alpha.2.tgz#2a8bc8dc6ecdb75fa6480ceeedc1f187c9539970"
Expand Down

0 comments on commit c254975

Please sign in to comment.