✨(react) add custom modal portal
In some apps this is mostly needed, for instance: when the CunninghamProvider is nested in the DOM, we want the modal to still be displayed on top of anything else, then in those cases we will be able to define a node directly in the body and tell cunningham to render modals inside it.
This commit is contained in:
@@ -44,6 +44,7 @@ interface ModalContextType {
|
||||
props?: Partial<ConfirmationModalProps>,
|
||||
) => Promise<Decision>;
|
||||
messageModal: (props?: Partial<MessageModalProps>) => Promise<Decision>;
|
||||
modalParentSelector?: () => HTMLElement;
|
||||
}
|
||||
|
||||
const ModalContext = createContext<undefined | ModalContextType>(undefined);
|
||||
@@ -102,7 +103,14 @@ const ModalContainer = ({
|
||||
|
||||
type ModalMap = Map<string, ReactNode>;
|
||||
|
||||
export const ModalProvider = ({ children }: PropsWithChildren) => {
|
||||
interface ModalProviderProps extends PropsWithChildren {
|
||||
modalParentSelector?: () => HTMLElement;
|
||||
}
|
||||
|
||||
export const ModalProvider = ({
|
||||
children,
|
||||
modalParentSelector,
|
||||
}: ModalProviderProps) => {
|
||||
const [modals, setModals] = useState<ModalMap>({} as ModalMap);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -151,6 +159,12 @@ export const ModalProvider = ({ children }: PropsWithChildren) => {
|
||||
deleteConfirmationModal: ModalHelper(DeleteConfirmationModal),
|
||||
confirmationModal: ModalHelper(ConfirmationModal),
|
||||
messageModal: ModalHelper(MessageModal),
|
||||
modalParentSelector: () => {
|
||||
if (modalParentSelector) {
|
||||
return modalParentSelector();
|
||||
}
|
||||
return document.getElementById("c__modals-portal")!;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -69,6 +69,43 @@ describe("<Modal/>", () => {
|
||||
expect(screen.getByText("Modal Content")).toBeInTheDocument();
|
||||
expect(app).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
it("use modalParentSelector to change the modal portal", async () => {
|
||||
const Wrapper = () => {
|
||||
const modal = useModal();
|
||||
return (
|
||||
<>
|
||||
<CunninghamProvider
|
||||
modalParentSelector={() =>
|
||||
document.querySelector("#my-custom-portal")!
|
||||
}
|
||||
>
|
||||
<button onClick={modal.open}>Open Modal</button>
|
||||
<Modal size={ModalSize.SMALL} {...modal}>
|
||||
<div>Modal Content</div>
|
||||
</Modal>
|
||||
</CunninghamProvider>
|
||||
<div id="my-custom-portal" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Wrapper />);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByText("Open Modal");
|
||||
const portal = document.querySelector("#my-custom-portal")!;
|
||||
|
||||
expect(portal.children.length).toEqual(0);
|
||||
expect(screen.queryByText("Modal Content")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(button);
|
||||
|
||||
const content = screen.getByText("Modal Content");
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(portal.children.length).toEqual(1);
|
||||
expect(portal.children[0].className).toEqual("ReactModalPortal");
|
||||
expect(portal.children[0].children.length).toEqual(1);
|
||||
expect(portal).toContain(content);
|
||||
});
|
||||
it("closes the modal when clicking on the close button", async () => {
|
||||
const Wrapper = () => {
|
||||
const modal = useModal();
|
||||
|
||||
@@ -21,10 +21,10 @@ const meta: Meta<typeof Modal> = {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<>
|
||||
<Button onClick={() => modal.open()}>Open Modal</Button>
|
||||
<Story args={{ ...context.args, ...modal }} />
|
||||
</CunninghamProvider>
|
||||
</>
|
||||
);
|
||||
},
|
||||
],
|
||||
@@ -197,3 +197,20 @@ export const FullWithContent: Story = {
|
||||
children: longLorem.text,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomParentSelect: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<CunninghamProvider
|
||||
modalParentSelector={() =>
|
||||
document.querySelector("#my-custom-modal-parent")!
|
||||
}
|
||||
>
|
||||
<Modal isOpen={true} onClose={() => {}} size={ModalSize.MEDIUM}>
|
||||
I am rendered inside #my-custom-modal-parent
|
||||
</Modal>
|
||||
<div id="my-custom-modal-parent" />
|
||||
</CunninghamProvider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { PropsWithChildren, ReactNode, useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import ReactModal from "react-modal";
|
||||
import { Button } from ":/components/Button";
|
||||
import { NOSCROLL_CLASS } from ":/components/Modal/ModalProvider";
|
||||
import { NOSCROLL_CLASS, useModals } from ":/components/Modal/ModalProvider";
|
||||
|
||||
export type ModalHandle = {};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Props extends PropsWithChildren {
|
||||
customLocales?: Record<string, TranslationSet>;
|
||||
currentLocale?: string;
|
||||
theme?: string;
|
||||
modalParentSelector?: () => HTMLElement;
|
||||
}
|
||||
|
||||
export const DEFAULT_LOCALE = Locales.enUS;
|
||||
@@ -53,6 +54,7 @@ export const CunninghamProvider = ({
|
||||
currentLocale = DEFAULT_LOCALE,
|
||||
customLocales,
|
||||
theme = DEFAULT_THEME,
|
||||
modalParentSelector,
|
||||
children,
|
||||
}: Props) => {
|
||||
const locales: Record<string, TranslationSet> = useMemo(
|
||||
@@ -102,7 +104,7 @@ export const CunninghamProvider = ({
|
||||
|
||||
return (
|
||||
<CunninghamContext.Provider value={context}>
|
||||
<ModalProvider>
|
||||
<ModalProvider modalParentSelector={modalParentSelector}>
|
||||
<div className="c__app">
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user