Skip to content
Closed
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
136 changes: 136 additions & 0 deletions react-responsive-modal/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,142 @@ describe('modal', () => {
});
});

describe('prop: keepMounted', () => {
it('should keep the modal mounted and hidden when closed', async () => {
const { getByTestId, rerender } = render(
<Modal open onClose={() => null} keepMounted animationDuration={0.01}>
<div>modal content</div>
</Modal>,
);

rerender(
<Modal
open={false}
onClose={() => null}
keepMounted
animationDuration={0.01}
>
<div>modal content</div>
</Modal>,
);

expect(getByTestId('modal')).toBeInTheDocument();
expect(getByTestId('root')).toHaveStyle({ visibility: 'hidden' });
});

it('should show the modal again when reopened', async () => {
const { getByTestId, rerender } = render(
<Modal open onClose={() => null} keepMounted animationDuration={0.01}>
<div>modal content</div>
</Modal>,
);

rerender(
<Modal
open={false}
onClose={() => null}
keepMounted
animationDuration={0.01}
>
<div>modal content</div>
</Modal>,
);
expect(getByTestId('root')).toHaveStyle({ visibility: 'hidden' });

rerender(
<Modal open onClose={() => null} keepMounted animationDuration={0.01}>
<div>modal content</div>
</Modal>,
);

expect(getByTestId('modal')).toBeInTheDocument();
expect(getByTestId('root')).not.toHaveStyle({ visibility: 'hidden' });
});

it('should preserve the DOM state of the modal content', async () => {
const { getByTestId, rerender } = render(
<Modal open onClose={() => null} keepMounted animationDuration={0.01}>
<input data-testid="input" />
</Modal>,
);

fireEvent.change(getByTestId('input'), { target: { value: 'hello' } });

rerender(
<Modal
open={false}
onClose={() => null}
keepMounted
animationDuration={0.01}
>
<input data-testid="input" />
</Modal>,
);

rerender(
<Modal open onClose={() => null} keepMounted animationDuration={0.01}>
<input data-testid="input" />
</Modal>,
);

expect(getByTestId('input')).toHaveValue('hello');
});

it('should call onAnimationEnd without unmounting when closed', async () => {
const onAnimationEnd = vitest.fn();
const { getByTestId, rerender } = render(
<Modal
open
onClose={() => null}
onAnimationEnd={onAnimationEnd}
keepMounted
animationDuration={0.01}
>
<div>modal content</div>
</Modal>,
);

rerender(
<Modal
open={false}
onClose={() => null}
onAnimationEnd={onAnimationEnd}
keepMounted
animationDuration={0.01}
>
<div>modal content</div>
</Modal>,
);
fireEvent.animationEnd(getByTestId('modal'));

expect(onAnimationEnd).toHaveBeenCalledTimes(1);
expect(getByTestId('modal')).toBeInTheDocument();
});

it('should not close on esc when the hidden modal is kept mounted', async () => {
const onClose = vitest.fn();
const { rerender } = render(
<Modal open onClose={onClose} keepMounted animationDuration={0.01}>
<div>modal content</div>
</Modal>,
);

rerender(
<Modal
open={false}
onClose={onClose}
keepMounted
animationDuration={0.01}
>
<div>modal content</div>
</Modal>,
);

fireEvent.keyDown(document, { keyCode: 27 });
expect(onClose).not.toHaveBeenCalled();
});
});

describe('prop: containerId', () => {
it('should renders container div with id', async () => {
const containerId = 'container-id';
Expand Down
26 changes: 21 additions & 5 deletions react-responsive-modal/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ export interface ModalProps {
* Callback fired when the Modal has exited and the animation is finished.
*/
onAnimationEnd?: (event: React.AnimationEvent<HTMLDivElement>) => void;
/**
* Keep the modal mounted in the DOM when it is closed. Useful to preserve
* the state of the content inside the modal.
*
* Default to false.
*/
keepMounted?: boolean;
children?: React.ReactNode;
}

Expand Down Expand Up @@ -195,6 +202,7 @@ export const Modal = React.forwardRef(
onEscKeyDown,
onOverlayClick,
onAnimationEnd,
keepMounted = false,
children,
reserveScrollBarGap,
dir,
Expand Down Expand Up @@ -282,11 +290,11 @@ export const Modal = React.forwardRef(
useEffect(() => {
// If the open prop is changing, we need to open the modal
// This is also called on the first render if the open prop is true when the modal is created
if (open && !showPortal) {
if ((open || keepMounted) && !showPortal) {
setShowPortal(true);
handleOpen();
}
}, [open]);
}, [open, keepMounted, showPortal]);

const handleClickOverlay = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
Expand Down Expand Up @@ -321,7 +329,7 @@ export const Modal = React.forwardRef(
return;
}

if (!open) {
if (!open && !keepMounted) {
setShowPortal(false);
}

Expand All @@ -342,7 +350,15 @@ export const Modal = React.forwardRef(
? createPortal(
<div
className={cx(classes.root, classNames?.root)}
style={styles?.root}
style={{
...styles?.root,
...(!open && keepMounted
? {
visibility: 'hidden',
transition: `visibility 0s linear ${animationDuration}ms`,
}
: {}),
}}
data-testid="root"
dir={dir}
>
Expand Down Expand Up @@ -387,7 +403,7 @@ export const Modal = React.forwardRef(
data-testid="modal"
tabIndex={-1}
>
{focusTrapped && (
{focusTrapped && (open || !keepMounted) && (
<FocusTrap
container={refDialog}
initialFocusRef={initialFocusRef}
Expand Down
2 changes: 2 additions & 0 deletions website/src/components/ExampleRendered.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CustomContainer from '../examples/CustomContainer';
import CustomCssStyle from '../examples/CustomCssStyle';
import FocusTrapped from '../examples/FocusTrapped';
import FocusTrappedInitialFocus from '../examples/FocusTrappedInitialFocus';
import KeepMounted from '../examples/KeepMounted';
import LongContent from '../examples/LongContent';
import ExampleMultiple from '../examples/Multiple';
import Simple from '../examples/Simple';
Expand All @@ -14,6 +15,7 @@ const examples: Record<string, () => React.ReactElement> = {
longContent: LongContent,
focusTrapped: FocusTrapped,
focusTrappedInitialFocus: FocusTrappedInitialFocus,
keepMounted: KeepMounted,
customCssStyle: CustomCssStyle,
customAnimation: CustomAnimation,
customCloseIcon: CustomCloseIcon,
Expand Down
11 changes: 11 additions & 0 deletions website/src/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ By default, the Modal will be rendered at the end of the html body tag. If you w

```

### Keeping the modal mounted

By default the Modal is unmounted from the DOM when it is closed. Set the `keepMounted` prop to keep it mounted while closed, so the state of the content inside the Modal is preserved. The Modal is hidden once the closing animation finishes, until it is opened again.

<ExampleRendered name="keepMounted" />

```js file=../examples/KeepMounted.tsx

```

## Accessibility

- Follow the [ARIA best practices](https://www.w3.org/TR/wai-aria-practices/#dialog_modal) by using either:
Expand Down Expand Up @@ -220,6 +230,7 @@ By default, the Modal will be rendered at the end of the html body tag. If you w
| **onEscKeyDown\*** | `(event: KeyboardEvent) => void` | | Callback fired when the escape key is pressed. |
| **onOverlayClick\*** | `(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void` | | Callback fired when the overlay is clicked. |
| **onAnimationEnd\*** | `(event: React.AnimationEvent<HTMLDivElement>) => void` | | Callback fired when the Modal has exited and the animation is finished. |
| **keepMounted** | `boolean` | false | Keep the modal mounted in the DOM when it is closed. Useful to preserve the state of the content inside the modal. |

## License

Expand Down
25 changes: 25 additions & 0 deletions website/src/examples/KeepMounted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Modal } from 'react-responsive-modal';

const App = () => {
const [open, setOpen] = React.useState(false);

return (
<>
<button className="button" onClick={() => setOpen(true)}>
Open modal
</button>

<Modal open={open} onClose={() => setOpen(false)} center keepMounted>
<h2>The modal is kept mounted</h2>
<p>
Type something in the input below, close the modal and reopen it. The
value is preserved because the modal is not unmounted when closed.
</p>
<input type="text" placeholder="This value is preserved" />
</Modal>
</>
);
};

export default App;
Loading