From 2916dd2af98bf7765d89c1663c3f104b1db159b4 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 4 Apr 2024 16:59:12 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(react)=20add=20custom=20modal=20porta?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/components/Modal/ModalProvider.tsx | 16 +++++++- .../react/src/components/Modal/index.spec.tsx | 37 +++++++++++++++++++ .../src/components/Modal/index.stories.tsx | 21 ++++++++++- packages/react/src/components/Modal/index.tsx | 2 +- .../react/src/components/Provider/index.tsx | 4 +- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/Modal/ModalProvider.tsx b/packages/react/src/components/Modal/ModalProvider.tsx index f9dab63..cad3cda 100644 --- a/packages/react/src/components/Modal/ModalProvider.tsx +++ b/packages/react/src/components/Modal/ModalProvider.tsx @@ -44,6 +44,7 @@ interface ModalContextType { props?: Partial, ) => Promise; messageModal: (props?: Partial) => Promise; + modalParentSelector?: () => HTMLElement; } const ModalContext = createContext(undefined); @@ -102,7 +103,14 @@ const ModalContainer = ({ type ModalMap = Map; -export const ModalProvider = ({ children }: PropsWithChildren) => { +interface ModalProviderProps extends PropsWithChildren { + modalParentSelector?: () => HTMLElement; +} + +export const ModalProvider = ({ + children, + modalParentSelector, +}: ModalProviderProps) => { const [modals, setModals] = useState({} 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")!; + }, }), [], ); diff --git a/packages/react/src/components/Modal/index.spec.tsx b/packages/react/src/components/Modal/index.spec.tsx index 26e089b..6c752b3 100644 --- a/packages/react/src/components/Modal/index.spec.tsx +++ b/packages/react/src/components/Modal/index.spec.tsx @@ -69,6 +69,43 @@ describe("", () => { 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 ( + <> + + document.querySelector("#my-custom-portal")! + } + > + + +
Modal Content
+
+
+
+ + ); + }; + + render(); + 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(); diff --git a/packages/react/src/components/Modal/index.stories.tsx b/packages/react/src/components/Modal/index.stories.tsx index 6f8738f..90d72b4 100644 --- a/packages/react/src/components/Modal/index.stories.tsx +++ b/packages/react/src/components/Modal/index.stories.tsx @@ -21,10 +21,10 @@ const meta: Meta = { }, []); return ( - + <> - + ); }, ], @@ -197,3 +197,20 @@ export const FullWithContent: Story = { children: longLorem.text, }, }; + +export const CustomParentSelect: Story = { + render: () => { + return ( + + document.querySelector("#my-custom-modal-parent")! + } + > + {}} size={ModalSize.MEDIUM}> + I am rendered inside #my-custom-modal-parent + +
+ + ); + }, +}; diff --git a/packages/react/src/components/Modal/index.tsx b/packages/react/src/components/Modal/index.tsx index 153a777..7367ab6 100644 --- a/packages/react/src/components/Modal/index.tsx +++ b/packages/react/src/components/Modal/index.tsx @@ -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 = {}; diff --git a/packages/react/src/components/Provider/index.tsx b/packages/react/src/components/Provider/index.tsx index adaf2b9..72dd1af 100644 --- a/packages/react/src/components/Provider/index.tsx +++ b/packages/react/src/components/Provider/index.tsx @@ -34,6 +34,7 @@ interface Props extends PropsWithChildren { customLocales?: Record; 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 = useMemo( @@ -102,7 +104,7 @@ export const CunninghamProvider = ({ return ( - +
{children}