From b4cde24e2c2552c5409134a907df03db8c4a1de6 Mon Sep 17 00:00:00 2001 From: rubinaga Date: Mon, 17 Nov 2025 18:58:03 +0100 Subject: [PATCH] feat(ConfirmationModal): introduce portal prop for DOM relocation --- .../ConfirmationButton/ConfirmationButton.tsx | 33 +++++++++---------- .../ConfirmationModal.test.tsx | 31 +++++++++++++++++ .../ConfirmationModal/ConfirmationModal.tsx | 12 ++++++- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/components/ConfirmationButton/ConfirmationButton.tsx b/src/components/ConfirmationButton/ConfirmationButton.tsx index 707af5d71..30c5d1fef 100644 --- a/src/components/ConfirmationButton/ConfirmationButton.tsx +++ b/src/components/ConfirmationButton/ConfirmationButton.tsx @@ -55,7 +55,7 @@ export const ConfirmationButton = ({ preModalOpenHook, ...actionButtonProps }: Props): React.JSX.Element => { - const { openPortal, closePortal, isOpen, Portal } = usePortal(); + const { openPortal, closePortal, isOpen } = usePortal(); const handleCancelModal = () => { closePortal(); @@ -90,22 +90,21 @@ export const ConfirmationButton = ({ return ( <> {isOpen && ( - - - {confirmationModalProps.children} - {showShiftClickHint && ( -

- Next time, you can skip this confirmation by holding{" "} - SHIFT and clicking the action. -

- )} -
-
+ + {confirmationModalProps.children} + {showShiftClickHint && ( +

+ Next time, you can skip this confirmation by holding{" "} + SHIFT and clicking the action. +

+ )} +
)} { "submit", ); }); + + it("renders without portal by default", () => { + const { container } = render( + + Test default rendering + , + ); + + const modal = container.querySelector(".p-modal"); + expect(modal).toBeInTheDocument(); + expect(container.contains(modal)).toBe(true); + }); + + it("renders inside a portal when renderInPortal is true", () => { + const { container } = render( + + Test portal rendering + , + ); + + const modal = document.querySelector(".p-modal"); + expect(modal).toBeInTheDocument(); + + expect(container.contains(modal)).toBe(false); + + expect(document.body.contains(modal)).toBe(true); + }); }); diff --git a/src/components/ConfirmationModal/ConfirmationModal.tsx b/src/components/ConfirmationModal/ConfirmationModal.tsx index 574788719..dfc950d58 100644 --- a/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -4,6 +4,7 @@ import { PropsWithSpread, ValueOf } from "types"; import Button, { ButtonAppearance, ButtonProps } from "components/Button"; import Modal, { ModalProps } from "components/Modal"; import ActionButton, { ActionButtonProps } from "components/ActionButton"; +import { usePortal } from "external"; export type Props = PropsWithSpread< { @@ -47,6 +48,10 @@ export type Props = PropsWithSpread< * Whether the confirm button should be disabled. */ confirmButtonDisabled?: boolean; + /** + * Whether to render the modal inside a Portal component. + */ + renderInPortal?: boolean; }, Omit >; @@ -65,8 +70,11 @@ export const ConfirmationModal = ({ confirmButtonLoading, confirmButtonDisabled, confirmButtonProps, + renderInPortal = false, ...props }: Props): React.JSX.Element => { + const { Portal } = usePortal(); + const handleClick = ( // eslint-disable-line @typescript-eslint/no-unsafe-function-type action: A | null | undefined, @@ -80,7 +88,7 @@ export const ConfirmationModal = ({ } }; - return ( + const ModalElement = ( @@ -110,6 +118,8 @@ export const ConfirmationModal = ({ {children} ); + + return renderInPortal ? {ModalElement} : ModalElement; }; export default ConfirmationModal;