1
1
import PropTypes from 'prop-types' ;
2
- import React , {
3
- useEffect ,
4
- useRef ,
5
- } from 'react' ;
2
+ import React , { useRef } from 'react' ;
6
3
import { createPortal } from 'react-dom' ;
7
4
import { withGlobalProps } from '../../provider' ;
8
5
import { transferProps } from '../_helpers/transferProps' ;
9
6
import { classNames } from '../../utils/classNames' ;
7
+ import { getPositionClassName } from './_helpers/getPositionClassName' ;
8
+ import { getSizeClassName } from './_helpers/getSizeClassName' ;
9
+ import { useModalFocus } from './_hooks/useModalFocus' ;
10
+ import { useModalScrollPrevention } from './_hooks/useModalScrollPrevention' ;
10
11
import styles from './Modal.scss' ;
11
12
12
13
const preRender = (
@@ -16,116 +17,56 @@ const preRender = (
16
17
position ,
17
18
restProps ,
18
19
size ,
19
- ) => {
20
- const sizeClass = ( modalSize ) => {
21
- if ( modalSize === 'small' ) {
22
- return styles . isRootSizeSmall ;
23
- }
24
-
25
- if ( modalSize === 'medium' ) {
26
- return styles . isRootSizeMedium ;
27
- }
28
-
29
- if ( modalSize === 'large' ) {
30
- return styles . isRootSizeLarge ;
31
- }
32
-
33
- if ( modalSize === 'fullscreen' ) {
34
- return styles . isRootSizeFullscreen ;
35
- }
36
-
37
- return styles . isRootSizeAuto ;
38
- } ;
39
-
40
- const positionClass = ( modalPosition ) => {
41
- if ( modalPosition === 'top' ) {
42
- return styles . isRootPositionTop ;
43
- }
44
-
45
- return styles . isRootPositionCenter ;
46
- } ;
47
-
48
- return (
20
+ ) => (
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
+ >
49
31
< div
50
- className = { styles . backdrop }
51
- onClick = { ( ) => {
52
- if ( closeButtonRef ?. current != null ) {
53
- closeButtonRef . current . click ( ) ;
54
- }
32
+ { ...transferProps ( restProps ) }
33
+ className = { classNames (
34
+ styles . root ,
35
+ getSizeClassName ( size , styles ) ,
36
+ getPositionClassName ( position , styles ) ,
37
+ ) }
38
+ onClick = { ( e ) => {
39
+ e . stopPropagation ( ) ;
55
40
} }
56
41
role = "presentation"
42
+ ref = { childrenWrapperRef }
57
43
>
58
- < div
59
- { ...transferProps ( restProps ) }
60
- className = { classNames (
61
- styles . root ,
62
- sizeClass ( size ) ,
63
- positionClass ( position ) ,
64
- ) }
65
- onClick = { ( e ) => {
66
- e . stopPropagation ( ) ;
67
- } }
68
- role = "presentation"
69
- ref = { childrenWrapperRef }
70
- >
71
- { children }
72
- </ div >
44
+ { children }
73
45
</ div >
74
- ) ;
75
- } ;
46
+ </ div >
47
+ ) ;
76
48
77
49
export const Modal = ( {
78
50
autoFocus,
79
51
children,
80
52
closeButtonRef,
81
53
portalId,
82
54
position,
55
+ preventScrollUnderneath,
83
56
primaryButtonRef,
84
57
size,
85
58
...restProps
86
59
} ) => {
87
60
const childrenWrapperRef = useRef ( ) ;
88
61
89
- const keyPressHandler = ( e ) => {
90
- if ( e . key === 'Escape' && closeButtonRef ?. current != null ) {
91
- closeButtonRef . current . click ( ) ;
92
- }
93
-
94
- if ( e . key === 'Enter' && e . target . nodeName !== 'BUTTON' && primaryButtonRef ?. current != null ) {
95
- primaryButtonRef . current . click ( ) ;
96
- }
97
- } ;
98
-
99
- useEffect ( ( ) => {
100
- window . document . addEventListener ( 'keydown' , keyPressHandler , false ) ;
101
- const removeKeyPressHandler = ( ) => {
102
- window . document . removeEventListener ( 'keydown' , keyPressHandler , false ) ;
103
- } ;
104
-
105
- // If `autoFocus` is set to `true`, following code finds first form field element
106
- // (input, textarea or select) or primary button and auto focuses it. This is necessary
107
- // to have focus on one of those elements to be able to submit form by pressing Enter key.
108
- if ( autoFocus ) {
109
- if ( childrenWrapperRef ?. current != null ) {
110
- const childrenWrapperElement = childrenWrapperRef . current ;
111
- const childrenElements = childrenWrapperElement . querySelectorAll ( '*' ) ;
112
- const formFieldEl = Array . from ( childrenElements ) . find (
113
- ( element ) => [ 'INPUT' , 'TEXTAREA' , 'SELECT' ] . includes ( element . nodeName ) && ! element . disabled ,
114
- ) ;
115
-
116
- if ( formFieldEl ) {
117
- formFieldEl . focus ( ) ;
118
- return removeKeyPressHandler ;
119
- }
120
- }
121
-
122
- if ( primaryButtonRef ?. current != null ) {
123
- primaryButtonRef . current . focus ( ) ;
124
- }
125
- }
62
+ useModalFocus (
63
+ autoFocus ,
64
+ childrenWrapperRef ,
65
+ primaryButtonRef ,
66
+ closeButtonRef ,
67
+ ) ;
126
68
127
- return removeKeyPressHandler ;
128
- } , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
69
+ useModalScrollPrevention ( preventScrollUnderneath ) ;
129
70
130
71
if ( portalId === null ) {
131
72
return preRender (
@@ -157,14 +98,16 @@ Modal.defaultProps = {
157
98
closeButtonRef : null ,
158
99
portalId : null ,
159
100
position : 'center' ,
101
+ preventScrollUnderneath : 'default' ,
160
102
primaryButtonRef : null ,
161
103
size : 'medium' ,
162
104
} ;
163
105
164
106
Modal . propTypes = {
165
107
/**
166
- * If `true`, focus the first input element in the modal or primary button (referenced by the `primaryButtonRef` prop)
167
- * when the modal is opened.
108
+ * If `true`, focus the first input element in the `Modal`, or primary button (referenced by the `primaryButtonRef`
109
+ * prop), or other focusable element when the `Modal` is opened. If there are none or `autoFocus` is set to `false`,
110
+ * focus the Modal itself.
168
111
*/
169
112
autoFocus : PropTypes . bool ,
170
113
/**
@@ -192,6 +135,24 @@ Modal.propTypes = {
192
135
* Vertical position of the modal inside browser window.
193
136
*/
194
137
position : PropTypes . oneOf ( [ 'top' , 'center' ] ) ,
138
+ /**
139
+ * Mode in which Modal prevents scroll of elements bellow:
140
+ * * `default` - Modal prevents scroll on the `body` element
141
+ * * `off` - Modal does not prevent any scroll
142
+ * * object
143
+ * * * `reset` - method called on Modal's unmount to reset scroll prevention
144
+ * * * `start` - method called on Modal's mount to custom scroll prevention
145
+ */
146
+ preventScrollUnderneath : PropTypes . oneOfType ( [
147
+ PropTypes . oneOf ( [
148
+ 'default' ,
149
+ 'off' ,
150
+ ] ) ,
151
+ PropTypes . shape ( {
152
+ reset : PropTypes . func ,
153
+ start : PropTypes . func ,
154
+ } ) ,
155
+ ] ) ,
195
156
/**
196
157
* Reference to primary button element. It is used to submit modal when Enter key is pressed and as fallback
197
158
* when `autoFocus` functionality does not find any input element to be focused.
0 commit comments