diff --git a/src/ImperativeTransition.tsx b/src/ImperativeTransition.tsx index 273c265..ac2559f 100644 --- a/src/ImperativeTransition.tsx +++ b/src/ImperativeTransition.tsx @@ -61,11 +61,9 @@ export function useTransition({ return ref; } -export interface ImperativeTransitionProps extends TransitionProps { +export interface ImperativeTransitionProps + extends Omit { transition: TransitionHandler; - appear: true; - mountOnEnter: true; - unmountOnExit: true; } /** diff --git a/src/Modal.tsx b/src/Modal.tsx index e50a2b0..78c3cb9 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -42,7 +42,7 @@ export interface RenderModalDialogProps { style: React.CSSProperties | undefined; className: string | undefined; tabIndex: number; - role: string; + role: string | undefined; ref: React.RefCallback; 'aria-modal': boolean | undefined; } @@ -184,6 +184,27 @@ export interface BaseModalProps extends TransitionCallbacks { restoreFocusOptions?: { preventScroll: boolean; }; + + /** + * Lazy mount the dialog element when the Modal is shown. + * + * @default true + */ + mountDialogOnEnter?: boolean | undefined; + + /** + * Unmount the dialog element (remove it from the DOM) when the modal is no longer visible. + * + * @default true + */ + unmountDialogOnExit?: boolean | undefined; + + /** + * Render modal in a portal. + * + * @default true + */ + portal?: boolean | undefined; } export interface ModalProps extends BaseModalProps { @@ -251,6 +272,9 @@ const Modal: React.ForwardRefExoticComponent< enforceFocus = true, restoreFocus = true, restoreFocusOptions, + mountDialogOnEnter = true, + unmountDialogOnExit = true, + portal = true, renderDialog, renderBackdrop = (props: RenderModalBackdropProps) =>
, manager: providedManager, @@ -349,10 +373,10 @@ const Modal: React.ForwardRefExoticComponent< // Show logic when: // - show is `true` _and_ `container` has resolved useEffect(() => { - if (!show || !container) return; + if (!show || (!container && portal)) return; handleShow(); - }, [show, container, /* should never change: */ handleShow]); + }, [show, container, portal, /* should never change: */ handleShow]); // Hide cleanup logic when: // - `exited` switches to true @@ -419,15 +443,15 @@ const Modal: React.ForwardRefExoticComponent< onExited?.(...args); }; - if (!container) { + if (!container && portal) { return null; } const dialogProps = { - role, + role: show ? role : undefined, ref: modal.setDialogRef, // apparently only works on the dialog role element - 'aria-modal': role === 'dialog' ? true : undefined, + 'aria-modal': show && role === 'dialog' ? true : undefined, ...rest, style, className, @@ -446,8 +470,8 @@ const Modal: React.ForwardRefExoticComponent< transition as TransitionComponent, runTransition, { - unmountOnExit: true, - mountOnEnter: true, + unmountOnExit: unmountDialogOnExit, + mountOnEnter: mountDialogOnEnter, appear: true, in: !!show, onExit, @@ -480,15 +504,18 @@ const Modal: React.ForwardRefExoticComponent< ); } - return ( + return portal && container ? ( + ReactDOM.createPortal( + <> + {backdropElement} + {dialog} + , + container, + ) + ) : ( <> - {ReactDOM.createPortal( - <> - {backdropElement} - {dialog} - , - container, - )} + {backdropElement} + {dialog} ); }, diff --git a/test/ModalSpec.tsx b/test/ModalSpec.tsx index 37eafb3..a2d7603 100644 --- a/test/ModalSpec.tsx +++ b/test/ModalSpec.tsx @@ -247,6 +247,31 @@ describe('', () => { ); }); + it('should not render in a portal when `portal` is false', () => { + render( +
+ + Message + +
, + ); + + expect( + screen.getByTestId('container').contains(screen.getByRole('dialog')), + ).toBeTruthy(); + }); + + it('should render the dialog when mountDialogOnEnter and mountDialogOnEnter are false when not shown', () => { + render( + + Message + , + { container: attachTo }, + ); + + expect(screen.getByText('Message')).toBeTruthy(); + }); + describe('Focused state', () => { let focusableContainer: HTMLElement;