diff --git a/packages/compass-components/src/hooks/use-confirmation.spec.tsx b/packages/compass-components/src/hooks/use-confirmation.spec.tsx
index 84f57b6852a..15b436401d1 100644
--- a/packages/compass-components/src/hooks/use-confirmation.spec.tsx
+++ b/packages/compass-components/src/hooks/use-confirmation.spec.tsx
@@ -1,5 +1,4 @@
import {
- cleanup,
render,
screen,
waitFor,
@@ -10,85 +9,30 @@ import {
import { expect } from 'chai';
import React from 'react';
-import {
- ConfirmationModalArea,
- useConfirmationModal,
- showConfirmation,
-} from './use-confirmation';
-
-const OpenConfirmationModalButton = () => {
- const { showConfirmation } = useConfirmationModal();
- return (
-
- );
-};
+import { ConfirmationModalArea, showConfirmation } from './use-confirmation';
describe('use-confirmation', function () {
- afterEach(cleanup);
-
- context('useConfirmationModal hook', function () {
- let modal: HTMLElement;
- beforeEach(function () {
- render(
-
-
-
- );
-
- userEvent.click(screen.getByText('Open Modal'));
- modal = screen.getByTestId('confirmation-modal');
- expect(modal).to.exist;
- });
-
- it('renders modal contents', function () {
- expect(within(modal).getByText('Are you sure?')).to.exist;
- expect(
- within(modal).getByText('This action can not be undone.')
- ).to.exist;
- expect(within(modal).getByText('Yes')).to.exist;
- expect(within(modal).getByText('Cancel')).to.exist;
- });
-
- it('handles cancel action', async function () {
- userEvent.click(within(modal).getByText('Cancel'));
- await waitForElementToBeRemoved(() =>
- screen.getByTestId('confirmation-modal')
- );
- });
-
- it('handles confirm action', async function () {
- userEvent.click(within(modal).getByText('Yes'));
- await waitForElementToBeRemoved(() =>
- screen.getByTestId('confirmation-modal')
- );
- });
- });
-
context('showConfirmation global function', function () {
let modal: HTMLElement;
let response: Promise;
beforeEach(async function () {
render(
-
+
);
- response = showConfirmation({
- title: 'Are you sure?',
- description: 'This action can not be undone.',
- buttonText: 'Yes',
- });
+ userEvent.click(screen.getByText('Open Modal'));
await waitFor(() => {
modal = screen.getByTestId('confirmation-modal');
});
@@ -105,12 +49,18 @@ describe('use-confirmation', function () {
it('handles cancel action', async function () {
userEvent.click(within(modal).getByText('Cancel'));
+ await waitForElementToBeRemoved(() =>
+ screen.getByTestId('confirmation-modal')
+ );
const confirmed = await response;
expect(confirmed).to.be.false;
});
it('handles confirm action', async function () {
userEvent.click(within(modal).getByText('Yes'));
+ await waitForElementToBeRemoved(() =>
+ screen.getByTestId('confirmation-modal')
+ );
const confirmed = await response;
expect(confirmed).to.be.true;
});
diff --git a/packages/compass-components/src/hooks/use-confirmation.tsx b/packages/compass-components/src/hooks/use-confirmation.tsx
index 1fd30fd1bfa..0a4b0727cff 100644
--- a/packages/compass-components/src/hooks/use-confirmation.tsx
+++ b/packages/compass-components/src/hooks/use-confirmation.tsx
@@ -1,4 +1,10 @@
-import React, { useContext, useEffect, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import { Variant as ConfirmationModalVariant } from '@leafygreen-ui/confirmation-modal';
import ConfirmationModal from '../components/modals/confirmation-modal';
import { css } from '@leafygreen-ui/emotion';
@@ -22,125 +28,100 @@ type ConfirmationProperties = Partial<
type ConfirmationCallback = (value: boolean) => void;
-interface ConfirmationModalContextData {
- showConfirmation: (props: ConfirmationProperties) => Promise;
- isMounted: boolean;
-}
-
-type ShowConfirmationEventDetail = {
- props: ConfirmationProperties & { confirmationId: number };
+type OnShowConfirmationProperties = {
+ props: ConfirmationProperties;
resolve: ConfirmationCallback;
reject: (err?: any) => void;
+ confirmationId: number;
};
-interface ConfirmationEventMap {
- 'show-confirmation': CustomEvent;
-}
-
-interface GlobalConfirmation extends EventTarget {
- addEventListener(
- type: K,
- listener: (this: GlobalConfirmation, ev: ConfirmationEventMap[K]) => void
- ): void;
- addEventListener(
- type: string,
- listener: EventListenerOrEventListenerObject
- ): void;
- removeEventListener(
- type: K,
- listener: (this: GlobalConfirmation, ev: ConfirmationEventMap[K]) => void
- ): void;
- removeEventListener(
- type: string,
- listener: EventListenerOrEventListenerObject
- ): void;
+interface ConfirmationModalActions {
+ showConfirmation: (props: ConfirmationProperties) => Promise;
}
-
-let confirmationId = 0;
-
-class GlobalConfirmation extends EventTarget {
+class GlobalConfirmationModalState implements ConfirmationModalActions {
+ private confirmationId = 0;
+ onShowCallback: ((props: OnShowConfirmationProperties) => void) | null = null;
showConfirmation(props: ConfirmationProperties) {
return new Promise((resolve, reject) => {
- this.dispatchEvent(
- new CustomEvent('show-confirmation', {
- detail: {
- props: { ...props, confirmationId: ++confirmationId },
- resolve,
- reject,
- },
- })
- );
+ this.onShowCallback?.({
+ props,
+ resolve,
+ reject,
+ confirmationId: ++this.confirmationId,
+ });
});
}
}
-const globalConfirmation = new GlobalConfirmation();
-
-export const showConfirmation =
- globalConfirmation.showConfirmation.bind(globalConfirmation);
-const ConfirmationModalContext =
- React.createContext({
- isMounted: false,
- showConfirmation,
- });
-
-type ConfirmationModalAreaProps = Partial<
- ShowConfirmationEventDetail['props']
-> & { open: boolean };
+const confirmationModalState = new GlobalConfirmationModalState();
+
+/**
+ * Programmatically show a
+ * [leafygreen ConfirmationModal](https://www.mongodb.design/component/confirmation-modal)
+ * component and (optionally) await on the user input. Can be used both inside
+ * and __outside__ React rendering tree. Useful when user needs to confirm an
+ * action before it can be executed or to prominently display information as a
+ * side-effect of some other user action.
+ *
+ * @example
+ * async function dropDatabaseAction() {
+ * const confirmed = await showConfirmation({
+ * variant: 'danger',
+ * title: 'Are you sure you want to drop database?',
+ * });
+ *
+ * if (confirmed) {
+ * dataService.dropDatabase(...)
+ * }
+ * }
+ *
+ * @example
+ *
+ *
+ * @param props ConfirmationModal rendering properties
+ */
+export const showConfirmation = confirmationModalState.showConfirmation.bind(
+ confirmationModalState
+);
const hideButtonStyles = css({
display: 'none !important',
});
-export const ConfirmationModalArea: React.FC = ({ children }) => {
- const hasParentContext = useContext(ConfirmationModalContext).isMounted;
-
- const [confirmationProps, setConfirmationProps] =
- useState({
- open: false,
- confirmationId: -1,
- });
+const _ConfirmationModalArea: React.FunctionComponent = ({ children }) => {
+ const [confirmationProps, setConfirmationProps] = useState<
+ Partial & { open: boolean; confirmationId: number }
+ >({
+ open: false,
+ confirmationId: -1,
+ });
const callbackRef = useRef();
-
- const listenerRef =
- useRef<(event: CustomEvent) => void>();
-
- const contextValue = React.useMemo(
- () => ({
- showConfirmation: (props: ConfirmationProperties) => {
- return new Promise((resolve, reject) => {
- const event = new CustomEvent(
- 'show-confirmation',
- {
- detail: {
- props: { ...props, confirmationId: ++confirmationId },
- resolve,
- reject,
- },
- }
- );
- listenerRef.current?.(event);
- });
- },
- isMounted: true,
- }),
- []
- );
-
- useEffect(() => {
- return () => {
- callbackRef.current?.(false);
- };
- }, []);
-
- // Event listener to use confirmation modal outside of react
- useEffect(() => {
- const listener = ({
- detail: { resolve, reject, props },
- }: CustomEvent) => {
- setConfirmationProps({ open: true, ...props });
+ const confirmationModalStateRef = useRef();
+
+ if (!confirmationModalStateRef.current) {
+ confirmationModalStateRef.current = confirmationModalState;
+ confirmationModalStateRef.current.onShowCallback = ({
+ props,
+ resolve,
+ reject,
+ confirmationId,
+ }) => {
+ setConfirmationProps({ open: true, confirmationId, ...props });
const onAbort = () => {
- setConfirmationProps({ open: false, ...props });
+ setConfirmationProps((state) => {
+ return { ...state, open: false };
+ });
reject(props.signal?.reason);
};
callbackRef.current = (confirmed) => {
@@ -149,40 +130,42 @@ export const ConfirmationModalArea: React.FC = ({ children }) => {
};
props.signal?.addEventListener('abort', onAbort);
};
- listenerRef.current = listener;
- globalConfirmation.addEventListener('show-confirmation', listener);
+ }
+
+ useEffect(() => {
return () => {
- globalConfirmation.removeEventListener('show-confirmation', listener);
+ callbackRef.current?.(false);
+ if (confirmationModalStateRef.current) {
+ confirmationModalStateRef.current.onShowCallback = null;
+ }
};
}, []);
- const handleConfirm = () => {
- onUserAction(true);
- };
-
- const handleCancel = () => {
- onUserAction(false);
- };
-
- const onUserAction = (value: boolean) => {
- setConfirmationProps((state) => ({ ...state, open: false }));
+ const onUserAction = useCallback((value: boolean) => {
+ setConfirmationProps((state) => {
+ return { ...state, open: false };
+ });
callbackRef.current?.(value);
callbackRef.current = undefined;
- };
+ }, []);
- if (hasParentContext) {
- return <>{children}>;
- }
+ const handleConfirm = useCallback(() => {
+ onUserAction(true);
+ }, [onUserAction]);
+
+ const handleCancel = useCallback(() => {
+ onUserAction(false);
+ }, [onUserAction]);
return (
-
+ <>
{children}
{
>
{confirmationProps.description}
-
+ >
);
};
-export const useConfirmationModal = () => {
- const { isMounted, showConfirmation } = useContext(ConfirmationModalContext);
- if (!isMounted) {
- throw new Error(
- 'useConfirmationModal must be used within a ConfirmationModalArea'
- );
+const ConfirmationModalAreaMountedContext = React.createContext(false);
+
+export const ConfirmationModalArea: React.FunctionComponent = ({
+ children,
+}) => {
+ if (useContext(ConfirmationModalAreaMountedContext)) {
+ return <>{children}>;
}
- return { showConfirmation };
+
+ return (
+
+ <_ConfirmationModalArea>{children}
+
+ );
};
diff --git a/packages/compass-components/src/hooks/use-error-details.tsx b/packages/compass-components/src/hooks/use-error-details.tsx
index 99302afa1c2..50a6ae270db 100644
--- a/packages/compass-components/src/hooks/use-error-details.tsx
+++ b/packages/compass-components/src/hooks/use-error-details.tsx
@@ -1,38 +1,30 @@
-import {
- type showConfirmation as originalShowConfirmation,
- showConfirmation,
-} from './use-confirmation';
+import { showConfirmation } from './use-confirmation';
import { Code } from '../components/leafygreen';
import React from 'react';
import { ButtonVariant } from '..';
-const getShowErrorDetails = (
- showConfirmation: typeof originalShowConfirmation
-) => {
- return ({
- details,
- closeAction,
- }: {
- details: Record;
- closeAction: 'back' | 'close';
- }) =>
- void showConfirmation({
- title: 'Error details',
- description: (
-
- {JSON.stringify(details, undefined, 2)}
-
- ),
- hideCancelButton: true,
- buttonText: closeAction.replace(/\b\w/g, (c) => c.toUpperCase()),
- confirmButtonProps: {
- variant: ButtonVariant.Default,
- },
- });
+export const showErrorDetails = function showErrorDetails({
+ details,
+ closeAction,
+}: {
+ details: Record;
+ closeAction: 'back' | 'close';
+}) {
+ void showConfirmation({
+ title: 'Error details',
+ description: (
+
+ {JSON.stringify(details, undefined, 2)}
+
+ ),
+ hideCancelButton: true,
+ buttonText: closeAction.replace(/\b\w/g, (c) => c.toUpperCase()),
+ confirmButtonProps: {
+ variant: ButtonVariant.Default,
+ },
+ });
};
-
-export const showErrorDetails = getShowErrorDetails(showConfirmation);
diff --git a/packages/compass-components/src/hooks/use-toast.spec.tsx b/packages/compass-components/src/hooks/use-toast.spec.tsx
index e8e91979254..c0f14df415c 100644
--- a/packages/compass-components/src/hooks/use-toast.spec.tsx
+++ b/packages/compass-components/src/hooks/use-toast.spec.tsx
@@ -1,5 +1,4 @@
import {
- cleanup,
render,
screen,
waitForElementToBeRemoved,
@@ -8,7 +7,7 @@ import {
import { expect } from 'chai';
import React from 'react';
-import { ToastArea, useToast } from './use-toast';
+import { ToastArea, openToast, closeToast } from './use-toast';
import type { ToastProperties } from './use-toast';
const OpenToastButton = ({
@@ -19,9 +18,13 @@ const OpenToastButton = ({
namespace: string;
id: string;
} & ToastProperties) => {
- const { openToast } = useToast(namespace);
return (
-