11import PropTypes from 'prop-types' ;
2- import React , { useRef } from 'react' ;
2+ import React , {
3+ useCallback ,
4+ useEffect ,
5+ useImperativeHandle ,
6+ useRef ,
7+ } from 'react' ;
38import { createPortal } from 'react-dom' ;
49import { withGlobalProps } from '../../providers/globalProps' ;
510import { classNames } from '../../utils/classNames' ;
611import { transferProps } from '../../utils/transferProps' ;
12+ import { dialogOnCancelHandler } from './_helpers/dialogOnCancelHandler' ;
13+ import { dialogOnClickHandler } from './_helpers/dialogOnClickHandler' ;
14+ import { dialogOnCloseHandler } from './_helpers/dialogOnCloseHandler' ;
15+ import { dialogOnKeyDownHandler } from './_helpers/dialogOnKeyDownHandler' ;
716import { getPositionClassName } from './_helpers/getPositionClassName' ;
817import { getSizeClassName } from './_helpers/getSizeClassName' ;
918import { useModalFocus } from './_hooks/useModalFocus' ;
@@ -12,90 +21,121 @@ import styles from './Modal.module.scss';
1221
1322const preRender = (
1423 children ,
15- childrenWrapperRef ,
16- closeButtonRef ,
24+ dialogRef ,
1725 position ,
18- restProps ,
1926 size ,
27+ events ,
28+ restProps ,
2029) => (
21- < div
22- className = { styles . backdrop }
23- onClick = { ( e ) => {
24- e . preventDefault ( ) ;
25- if ( closeButtonRef ?. current != null ) {
26- closeButtonRef . current . click ( ) ;
27- }
28- } }
29- role = "presentation"
30+ < dialog
31+ { ... transferProps ( restProps ) }
32+ { ... transferProps ( events ) }
33+ className = { classNames (
34+ styles . root ,
35+ getSizeClassName ( size , styles ) ,
36+ getPositionClassName ( position , styles ) ,
37+ ) }
38+ ref = { dialogRef }
3039 >
31- < div
32- { ...transferProps ( restProps ) }
33- className = { classNames (
34- styles . root ,
35- getSizeClassName ( size , styles ) ,
36- getPositionClassName ( position , styles ) ,
37- ) }
38- onClick = { ( e ) => {
39- e . stopPropagation ( ) ;
40- } }
41- ref = { childrenWrapperRef }
42- role = "presentation"
43- >
44- { children }
45- </ div >
46- </ div >
40+ { children }
41+ </ dialog >
4742) ;
4843
4944export const Modal = ( {
45+ allowCloseOnBackdropClick,
46+ allowCloseOnEscapeKey,
47+ allowPrimaryActionOnEnterKey,
5048 autoFocus,
5149 children,
5250 closeButtonRef,
51+ dialogRef,
5352 portalId,
5453 position,
5554 preventScrollUnderneath,
5655 primaryButtonRef,
5756 size,
5857 ...restProps
5958} ) => {
60- const childrenWrapperRef = useRef ( ) ;
59+ const internalDialogRef = useRef ( ) ;
6160
62- useModalFocus (
63- autoFocus ,
64- childrenWrapperRef ,
65- primaryButtonRef ,
66- closeButtonRef ,
67- ) ;
61+ useEffect ( ( ) => {
62+ internalDialogRef . current . showModal ( ) ;
63+ } , [ ] ) ;
6864
65+ // We need to have a reference to the dialog element to be able to call its methods,
66+ // but at the same time we want to expose this reference to the parent component for
67+ // case someone wants to call dialog methods from outside the component.
68+ useImperativeHandle ( dialogRef , ( ) => internalDialogRef . current ) ;
69+
70+ useModalFocus ( autoFocus , internalDialogRef , primaryButtonRef ) ;
6971 useModalScrollPrevention ( preventScrollUnderneath ) ;
7072
73+ const onCancel = useCallback (
74+ ( e ) => dialogOnCancelHandler ( e , closeButtonRef , restProps . onCancel ) ,
75+ [ closeButtonRef , restProps . onCancel ] ,
76+ ) ;
77+ const onClick = useCallback (
78+ ( e ) => dialogOnClickHandler ( e , closeButtonRef , internalDialogRef , allowCloseOnBackdropClick ) ,
79+ [ allowCloseOnBackdropClick , closeButtonRef , internalDialogRef ] ,
80+ ) ;
81+ const onClose = useCallback (
82+ ( e ) => dialogOnCloseHandler ( e , closeButtonRef , restProps . onClose ) ,
83+ [ closeButtonRef , restProps . onClose ] ,
84+ ) ;
85+ const onKeyDown = useCallback (
86+ ( e ) => dialogOnKeyDownHandler (
87+ e ,
88+ closeButtonRef ,
89+ primaryButtonRef ,
90+ allowCloseOnEscapeKey ,
91+ allowPrimaryActionOnEnterKey ,
92+ ) ,
93+ [
94+ allowCloseOnEscapeKey ,
95+ allowPrimaryActionOnEnterKey ,
96+ closeButtonRef ,
97+ primaryButtonRef ,
98+ ] ,
99+ ) ;
100+ const events = {
101+ onCancel,
102+ onClick,
103+ onClose,
104+ onKeyDown,
105+ } ;
106+
71107 if ( portalId === null ) {
72108 return preRender (
73109 children ,
74- childrenWrapperRef ,
75- closeButtonRef ,
110+ internalDialogRef ,
76111 position ,
77- restProps ,
78112 size ,
113+ events ,
114+ restProps ,
79115 ) ;
80116 }
81117
82118 return createPortal (
83119 preRender (
84120 children ,
85- childrenWrapperRef ,
86- closeButtonRef ,
121+ internalDialogRef ,
87122 position ,
88- restProps ,
89123 size ,
124+ events ,
125+ restProps ,
90126 ) ,
91127 document . getElementById ( portalId ) ,
92128 ) ;
93129} ;
94130
95131Modal . defaultProps = {
132+ allowCloseOnBackdropClick : true ,
133+ allowCloseOnEscapeKey : true ,
134+ allowPrimaryActionOnEnterKey : true ,
96135 autoFocus : true ,
97136 children : null ,
98137 closeButtonRef : null ,
138+ dialogRef : null ,
99139 portalId : null ,
100140 position : 'center' ,
101141 preventScrollUnderneath : window . document . body ,
@@ -104,6 +144,18 @@ Modal.defaultProps = {
104144} ;
105145
106146Modal . propTypes = {
147+ /**
148+ * If `true`, the `Modal` can be closed by clicking on the backdrop.
149+ */
150+ allowCloseOnBackdropClick : PropTypes . bool ,
151+ /**
152+ * If `true`, the `Modal` can be closed by pressing the Escape key.
153+ */
154+ allowCloseOnEscapeKey : PropTypes . bool ,
155+ /**
156+ * If `true`, the `Modal` can be submitted by pressing the Enter key.
157+ */
158+ allowPrimaryActionOnEnterKey : PropTypes . bool ,
107159 /**
108160 * If `true`, focus the first input element in the `Modal`, or primary button (referenced by the `primaryButtonRef`
109161 * prop), or other focusable element when the `Modal` is opened. If there are none or `autoFocus` is set to `false`,
@@ -121,12 +173,20 @@ Modal.propTypes = {
121173 */
122174 children : PropTypes . node ,
123175 /**
124- * Reference to close button element. It is used to close modal when Escape key is pressed or the backdrop is clicked.
176+ * Reference to close button element. It is used to close modal when Escape key is pressed
177+ * or the backdrop is clicked.
125178 */
126179 closeButtonRef : PropTypes . shape ( {
127180 // eslint-disable-next-line react/forbid-prop-types
128181 current : PropTypes . any ,
129182 } ) ,
183+ /**
184+ * Reference to dialog element
185+ */
186+ dialogRef : PropTypes . shape ( {
187+ // eslint-disable-next-line react/forbid-prop-types
188+ current : PropTypes . any ,
189+ } ) ,
130190 /**
131191 * If set, modal is rendered in the React Portal with that ID.
132192 */
0 commit comments