Skip to content

Commit 484723f

Browse files
authored
feat(Modal): add new render options for modal (#122)
1 parent 5170d38 commit 484723f

File tree

3 files changed

+70
-20
lines changed

3 files changed

+70
-20
lines changed

src/ImperativeTransition.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,9 @@ export function useTransition({
6161
return ref;
6262
}
6363

64-
export interface ImperativeTransitionProps extends TransitionProps {
64+
export interface ImperativeTransitionProps
65+
extends Omit<TransitionProps, 'appear' | 'mountOnEnter' | 'unmountOnExit'> {
6566
transition: TransitionHandler;
66-
appear: true;
67-
mountOnEnter: true;
68-
unmountOnExit: true;
6967
}
7068

7169
/**

src/Modal.tsx

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export interface RenderModalDialogProps {
4242
style: React.CSSProperties | undefined;
4343
className: string | undefined;
4444
tabIndex: number;
45-
role: string;
45+
role: string | undefined;
4646
ref: React.RefCallback<Element>;
4747
'aria-modal': boolean | undefined;
4848
}
@@ -184,6 +184,27 @@ export interface BaseModalProps extends TransitionCallbacks {
184184
restoreFocusOptions?: {
185185
preventScroll: boolean;
186186
};
187+
188+
/**
189+
* Lazy mount the dialog element when the Modal is shown.
190+
*
191+
* @default true
192+
*/
193+
mountDialogOnEnter?: boolean | undefined;
194+
195+
/**
196+
* Unmount the dialog element (remove it from the DOM) when the modal is no longer visible.
197+
*
198+
* @default true
199+
*/
200+
unmountDialogOnExit?: boolean | undefined;
201+
202+
/**
203+
* Render modal in a portal.
204+
*
205+
* @default true
206+
*/
207+
portal?: boolean | undefined;
187208
}
188209

189210
export interface ModalProps extends BaseModalProps {
@@ -251,6 +272,9 @@ const Modal: React.ForwardRefExoticComponent<
251272
enforceFocus = true,
252273
restoreFocus = true,
253274
restoreFocusOptions,
275+
mountDialogOnEnter = true,
276+
unmountDialogOnExit = true,
277+
portal = true,
254278
renderDialog,
255279
renderBackdrop = (props: RenderModalBackdropProps) => <div {...props} />,
256280
manager: providedManager,
@@ -349,10 +373,10 @@ const Modal: React.ForwardRefExoticComponent<
349373
// Show logic when:
350374
// - show is `true` _and_ `container` has resolved
351375
useEffect(() => {
352-
if (!show || !container) return;
376+
if (!show || (!container && portal)) return;
353377

354378
handleShow();
355-
}, [show, container, /* should never change: */ handleShow]);
379+
}, [show, container, portal, /* should never change: */ handleShow]);
356380

357381
// Hide cleanup logic when:
358382
// - `exited` switches to true
@@ -419,15 +443,15 @@ const Modal: React.ForwardRefExoticComponent<
419443
onExited?.(...args);
420444
};
421445

422-
if (!container) {
446+
if (!container && portal) {
423447
return null;
424448
}
425449

426450
const dialogProps = {
427-
role,
451+
role: show ? role : undefined,
428452
ref: modal.setDialogRef,
429453
// apparently only works on the dialog role element
430-
'aria-modal': role === 'dialog' ? true : undefined,
454+
'aria-modal': show && role === 'dialog' ? true : undefined,
431455
...rest,
432456
style,
433457
className,
@@ -446,8 +470,8 @@ const Modal: React.ForwardRefExoticComponent<
446470
transition as TransitionComponent,
447471
runTransition,
448472
{
449-
unmountOnExit: true,
450-
mountOnEnter: true,
473+
unmountOnExit: unmountDialogOnExit,
474+
mountOnEnter: mountDialogOnEnter,
451475
appear: true,
452476
in: !!show,
453477
onExit,
@@ -480,15 +504,18 @@ const Modal: React.ForwardRefExoticComponent<
480504
);
481505
}
482506

483-
return (
507+
return portal && container ? (
508+
ReactDOM.createPortal(
509+
<>
510+
{backdropElement}
511+
{dialog}
512+
</>,
513+
container,
514+
)
515+
) : (
484516
<>
485-
{ReactDOM.createPortal(
486-
<>
487-
{backdropElement}
488-
{dialog}
489-
</>,
490-
container,
491-
)}
517+
{backdropElement}
518+
{dialog}
492519
</>
493520
);
494521
},

test/ModalSpec.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,31 @@ describe('<Modal>', () => {
247247
);
248248
});
249249

250+
it('should not render in a portal when `portal` is false', () => {
251+
render(
252+
<div data-testid="container">
253+
<Modal show portal={false}>
254+
<strong>Message</strong>
255+
</Modal>
256+
</div>,
257+
);
258+
259+
expect(
260+
screen.getByTestId('container').contains(screen.getByRole('dialog')),
261+
).toBeTruthy();
262+
});
263+
264+
it('should render the dialog when mountDialogOnEnter and mountDialogOnEnter are false when not shown', () => {
265+
render(
266+
<Modal mountDialogOnEnter={false} unmountDialogOnExit={false}>
267+
<strong>Message</strong>
268+
</Modal>,
269+
{ container: attachTo },
270+
);
271+
272+
expect(screen.getByText('Message')).toBeTruthy();
273+
});
274+
250275
describe('Focused state', () => {
251276
let focusableContainer: HTMLElement;
252277

0 commit comments

Comments
 (0)