diff --git a/src/components/ConfirmationButton/ConfirmationButton.test.tsx b/src/components/ConfirmationButton/ConfirmationButton.test.tsx index a9c0ceb1..6d6abc0c 100644 --- a/src/components/ConfirmationButton/ConfirmationButton.test.tsx +++ b/src/components/ConfirmationButton/ConfirmationButton.test.tsx @@ -156,4 +156,53 @@ describe("ConfirmationButton ", () => { expect(shouldShowModal).toHaveBeenCalled(); expect(screen.getByText("Confirm")).toBeInTheDocument(); }); + + it("executes onConfirm when clicking the modal confirm button", async () => { + const onConfirm = jest.fn(); + render( + + Delete + , + ); + + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: "Delete" })); + await user.click(screen.getByRole("button", { name: "Proceed" })); + + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it("ignores portal overrides passed through confirmationModalProps", async () => { + const onConfirm = jest.fn(); + render( + ( +
{children}
+ ), + } as unknown as React.ComponentProps< + typeof ConfirmationButton + >["confirmationModalProps"] + } + > + Delete +
, + ); + + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: "Delete" })); + await user.click(screen.getByRole("button", { name: "Proceed" })); + + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId("unsafe-portal")).not.toBeInTheDocument(); + }); }); diff --git a/src/components/ConfirmationButton/ConfirmationButton.tsx b/src/components/ConfirmationButton/ConfirmationButton.tsx index 30c5d1fe..558358f2 100644 --- a/src/components/ConfirmationButton/ConfirmationButton.tsx +++ b/src/components/ConfirmationButton/ConfirmationButton.tsx @@ -20,8 +20,11 @@ export type Props = PropsWithSpread< { /** * Additional props to pass to the confirmation modal. + * The `renderInPortal` and `portalRenderer` props are controlled internally by this component. */ - confirmationModalProps: SubComponentProps; + confirmationModalProps: SubComponentProps< + Omit + >; /** * An optional text to be shown when hovering over the button.
* Defaults to the label of the confirm button in the modal. @@ -55,7 +58,12 @@ export const ConfirmationButton = ({ preModalOpenHook, ...actionButtonProps }: Props): React.JSX.Element => { - const { openPortal, closePortal, isOpen } = usePortal(); + const { openPortal, closePortal, isOpen, Portal } = usePortal(); + const { + renderInPortal: _ignoredRenderInPortal, + portalRenderer: _ignoredPortalRenderer, + ...safeConfirmationModalProps + } = confirmationModalProps as SubComponentProps; const handleCancelModal = () => { closePortal(); @@ -91,11 +99,11 @@ export const ConfirmationButton = ({ <> {isOpen && ( {confirmationModalProps.children} {showShiftClickHint && ( diff --git a/src/components/ConfirmationModal/ConfirmationModal.test.tsx b/src/components/ConfirmationModal/ConfirmationModal.test.tsx index 5875f53a..fab8f92b 100644 --- a/src/components/ConfirmationModal/ConfirmationModal.test.tsx +++ b/src/components/ConfirmationModal/ConfirmationModal.test.tsx @@ -181,4 +181,31 @@ describe("ConfirmationModal ", () => { expect(document.body.contains(modal)).toBe(true); }); + + it("prioritises portalRenderer over renderInPortal", () => { + const PortalRenderer = ({ + children, + }: { + children: React.ReactNode; + }): React.JSX.Element => ( +
{children}
+ ); + + render( + + Test custom portal renderer + , + ); + + const modal = document.querySelector(".p-modal"); + expect(modal).toBeInTheDocument(); + expect(screen.getByTestId("custom-portal-renderer")).toContainElement( + modal, + ); + }); }); diff --git a/src/components/ConfirmationModal/ConfirmationModal.tsx b/src/components/ConfirmationModal/ConfirmationModal.tsx index dfc950d5..fa4fed8e 100644 --- a/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -52,10 +52,24 @@ export type Props = PropsWithSpread< * Whether to render the modal inside a Portal component. */ renderInPortal?: boolean; + /** + * Optional custom portal renderer. If provided, it takes precedence + * over `renderInPortal`. + */ + portalRenderer?: React.ComponentType<{ children: ReactNode }>; }, Omit >; +const InternalPortalRenderer = ({ + children, +}: { + children: ReactNode; +}): React.JSX.Element => { + const { Portal } = usePortal(); + return {children}; +}; + /** * `ConfirmationModal` is a specialised version of the [Modal](?path=/docs/modal--default-story) component to prompt a confirmation from the user before executing an action. */ @@ -71,10 +85,9 @@ export const ConfirmationModal = ({ confirmButtonDisabled, confirmButtonProps, renderInPortal = false, + portalRenderer: PortalRenderer, ...props }: Props): React.JSX.Element => { - const { Portal } = usePortal(); - const handleClick = ( // eslint-disable-line @typescript-eslint/no-unsafe-function-type action: A | null | undefined, @@ -119,7 +132,13 @@ export const ConfirmationModal = ({ ); - return renderInPortal ? {ModalElement} : ModalElement; + if (PortalRenderer) { + return {ModalElement}; + } else if (renderInPortal) { + return {ModalElement}; + } else { + return ModalElement; + } }; export default ConfirmationModal;