Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/components/ConfirmationButton/ConfirmationButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ConfirmationButton
confirmationModalProps={{
confirmButtonLabel: "Proceed",
onConfirm,
}}
>
Delete
</ConfirmationButton>,
);

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(
<ConfirmationButton
confirmationModalProps={
{
confirmButtonLabel: "Proceed",
onConfirm,
renderInPortal: true,
portalRenderer: ({ children }: { children: React.ReactNode }) => (
<div data-testid="unsafe-portal">{children}</div>
),
} as unknown as React.ComponentProps<
typeof ConfirmationButton
>["confirmationModalProps"]
}
>
Delete
</ConfirmationButton>,
);

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();
});
});
16 changes: 12 additions & 4 deletions src/components/ConfirmationButton/ConfirmationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
confirmationModalProps: SubComponentProps<
Omit<ConfirmationModalProps, "renderInPortal" | "portalRenderer">
>;
/**
* An optional text to be shown when hovering over the button.<br/>
* Defaults to the label of the confirm button in the modal.
Expand Down Expand Up @@ -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<ConfirmationModalProps>;

const handleCancelModal = () => {
closePortal();
Expand Down Expand Up @@ -91,11 +99,11 @@ export const ConfirmationButton = ({
<>
{isOpen && (
<ConfirmationModal
{...confirmationModalProps}
{...safeConfirmationModalProps}
close={handleCancelModal}
confirmButtonLabel={confirmationModalProps.confirmButtonLabel}
onConfirm={handleConfirmModal}
renderInPortal={true}
portalRenderer={Portal}
>
{confirmationModalProps.children}
{showShiftClickHint && (
Expand Down
27 changes: 27 additions & 0 deletions src/components/ConfirmationModal/ConfirmationModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => (
<div data-testid="custom-portal-renderer">{children}</div>
);

render(
<ConfirmationModal
confirmButtonLabel="Proceed"
onConfirm={jest.fn()}
renderInPortal={true}
portalRenderer={PortalRenderer}
>
Test custom portal renderer
</ConfirmationModal>,
);

const modal = document.querySelector<HTMLElement>(".p-modal");
expect(modal).toBeInTheDocument();
expect(screen.getByTestId("custom-portal-renderer")).toContainElement(
modal,
);
});
});
25 changes: 22 additions & 3 deletions src/components/ConfirmationModal/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModalProps, "buttonRow">
>;

const InternalPortalRenderer = ({
children,
}: {
children: ReactNode;
}): React.JSX.Element => {
const { Portal } = usePortal();
return <Portal>{children}</Portal>;
};

/**
* `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.
*/
Expand All @@ -71,10 +85,9 @@ export const ConfirmationModal = ({
confirmButtonDisabled,
confirmButtonProps,
renderInPortal = false,
portalRenderer: PortalRenderer,
...props
}: Props): React.JSX.Element => {
const { Portal } = usePortal();

const handleClick =
<A extends Function>( // eslint-disable-line @typescript-eslint/no-unsafe-function-type
action: A | null | undefined,
Expand Down Expand Up @@ -119,7 +132,13 @@ export const ConfirmationModal = ({
</Modal>
);

return renderInPortal ? <Portal>{ModalElement}</Portal> : ModalElement;
if (PortalRenderer) {
return <PortalRenderer>{ModalElement}</PortalRenderer>;
} else if (renderInPortal) {
return <InternalPortalRenderer>{ModalElement}</InternalPortalRenderer>;
} else {
return ModalElement;
}
};

export default ConfirmationModal;
Loading