Skip to content

Commit

Permalink
feat(modal): added new isModalClosingControlledManually property (#1194)
Browse files Browse the repository at this point in the history
Co-authored-by: Ihor Fesyk <[email protected]>
  • Loading branch information
IgorFesyk and Ihor Fesyk authored Feb 20, 2024
1 parent 36adb34 commit 9a536bc
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/modal-isModalClosingControlledManually.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-magma-dom': patch
---

feat(modal): Added new isModalClosingControlledManually property that allows handling closing the modal on the consumer side
69 changes: 68 additions & 1 deletion packages/react-magma-dom/src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DropdownMenuItem,
} from '../Dropdown';
import { Select } from '../Select';
import { useFocusLock } from '../..';

const info = {
component: Modal,
Expand Down Expand Up @@ -407,6 +408,72 @@ export const ModalInAModal = () => {
);
};

export const CloseModalWithConfirmation = () => {
const [showModal, setShowModal] = React.useState(false);
const [showConfirmationModal, setShowConfirmationModal] =
React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement>();
const focusTrapElement = useFocusLock(!showConfirmationModal && showModal);

const closeTheModal = () => {
setShowConfirmationModal(true);
};

const closeTheConfirmationModal = () => {
setShowConfirmationModal(false);
};

const closeBothModals = () => {
buttonRef.current.focus();
setShowConfirmationModal(false);
setShowModal(false);
};

return (
<>
<Button onClick={() => setShowModal(true)} ref={buttonRef}>
Show Modal
</Button>
<Modal
header="Modal Title"
isModalClosingControlledManually
onClose={closeTheModal}
isOpen={showModal}
ref={focusTrapElement}
>
<Paragraph noTopMargin>This is a modal, doing modal things.</Paragraph>
<Paragraph>
This is <a href="/">linked text</a> in the modal
</Paragraph>
<Combobox
id="comboboxId3"
isMulti
labelText="Multi Combobox"
defaultItems={[
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue' },
{ label: 'Green', value: 'green' },
]}
placeholder="Hello"
/>
</Modal>
<Modal
size={ModalSize.small}
header="Confirmation Modal"
isModalClosingControlledManually
onClose={closeTheConfirmationModal}
isOpen={showConfirmationModal}
>
<Paragraph noTopMargin>Close the modal?</Paragraph>
<ButtonGroup>
<Button onClick={closeBothModals}>Yes</Button>
<Button onClick={closeTheConfirmationModal}>No, go back</Button>
</ButtonGroup>
</Modal>
</>
);
};

export const Inverse = () => {
const [showModal, setShowModal] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement>();
Expand Down Expand Up @@ -437,4 +504,4 @@ export const Inverse = () => {
</Container>
</>
);
};
};
172 changes: 171 additions & 1 deletion packages/react-magma-dom/src/components/Modal/Modal.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { Modal } from '.';
import { act, render, fireEvent, queryByText } from '@testing-library/react';
import { act, render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { I18nContext } from '../../i18n';
import { defaultI18n } from '../../i18n/default';
Expand Down Expand Up @@ -234,6 +234,86 @@ describe('Modal', () => {
expect(onCloseSpy).toHaveBeenCalled();
});

it('should close when isModalClosingControlledManually is true and isOpen prop changed to false', async () => {
const onCloseSpy = jest.fn();
const { rerender, queryByText } = render(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={true}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

rerender(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={false}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

await act(async () => {
jest.runAllTimers();
});

expect(onCloseSpy).not.toHaveBeenCalled();
expect(queryByText('Modal Content')).not.toBeInTheDocument();
});

it('should not force close when clicking the close button if isModalClosingControlledManually is true', async () => {
const onCloseSpy = jest.fn();
const { rerender, getByText, getByTestId } = render(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={false}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.focus(getByText('Open'));

rerender(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={true}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.click(getByTestId('modal-closebtn'));

await act(async () => {
jest.runAllTimers();
});

expect(onCloseSpy).toHaveBeenCalled();
expect(getByText('Modal Content')).toBeInTheDocument();
});

it('should close when pressing the escape button', async () => {
const onCloseSpy = jest.fn();
const { rerender, getByText } = render(
Expand Down Expand Up @@ -268,6 +348,51 @@ describe('Modal', () => {
expect(onCloseSpy).toHaveBeenCalled();
});

it('should not force close when pressing the escape button if isModalClosingControlledManually is true', async () => {
const onCloseSpy = jest.fn();
const { rerender, getByText } = render(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={false}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.focus(getByText('Open'));

rerender(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={true}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.keyDown(getByText('Modal Content'), {
key: 'Escape',
keyCode: 27,
});

await act(async () => {
jest.runAllTimers();
});

expect(onCloseSpy).toHaveBeenCalled();
expect(getByText('Modal Content')).toBeInTheDocument();
});

it('should call the passed in onEscKeyDown function', async () => {
const onEscKeyDown = jest.fn();
const { rerender, getByText } = render(
Expand Down Expand Up @@ -350,6 +475,51 @@ describe('Modal', () => {
expect(onCloseSpy).toHaveBeenCalled();
});

it('should not force close when clicking on the backdrop if isModalClosingControlledManually is true', async () => {
const testId = 'modal-container';
const onCloseSpy = jest.fn();
const { rerender, getByText, getByTestId } = render(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={false}
onClose={onCloseSpy}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.focus(getByText('Open'));

rerender(
<>
<button>Open</button>
<Modal
header="Hello"
isOpen={true}
onClose={onCloseSpy}
testId={testId}
isModalClosingControlledManually
>
Modal Content
</Modal>
</>
);

fireEvent.mouseDown(getByTestId(testId));
fireEvent.click(getByTestId(testId));

await act(async () => {
jest.runAllTimers();
});

expect(onCloseSpy).toHaveBeenCalled();
expect(getByText('Modal Content')).toBeInTheDocument();
});

it('should not close when mouse in happened inside the modal but mouse up happened on the backdrop', async () => {
const testId = 'modal-container';
const modalContent = 'Modal Content';
Expand Down
19 changes: 17 additions & 2 deletions packages/react-magma-dom/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
* The content of the modal header
*/
header?: React.ReactNode;
/**
* If true, closing the modal handled on the consumer side
* @default false
*/
isModalClosingControlledManually?: boolean;
/**
* If true, clicking the backdrop will not dismiss the modal
* @default false
Expand Down Expand Up @@ -223,7 +228,14 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
const prevOpen = usePrevious(props.isOpen);

React.useEffect(() => {
if (!prevOpen && props.isOpen) {
if (
props.isModalClosingControlledManually &&
prevOpen &&
!props.isOpen &&
isModalOpen
) {
setIsModalOpen(false);
} else if (!prevOpen && props.isOpen) {
setIsModalOpen(true);
} else if (prevOpen && !props.isOpen && isModalOpen) {
handleClose();
Expand Down Expand Up @@ -298,7 +310,10 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(

setTimeout(() => {
setIsExiting(false);
setIsModalOpen(false);

if (!props.isModalClosingControlledManually) {
setIsModalOpen(false);
}

if (lastFocus.current) {
lastFocus.current.focus();
Expand Down
Loading

2 comments on commit 9a536bc

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.