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;