react-responsive-modal 5.2.6 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-responsive-modal",
3
- "version": "5.2.6",
3
+ "version": "6.2.0",
4
4
  "description": "A simple responsive and accessible react modal",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -12,10 +12,7 @@
12
12
  "test": "tsdx test --passWithNoTests",
13
13
  "lint": "tsdx lint",
14
14
  "prepare": "tsdx build",
15
- "docz:dev": "docz dev",
16
- "docz:build": "docz build",
17
- "size": "size-limit",
18
- "prettier": "prettier --write \"**/*.{js,ts,tsx,css,scss,json,md,mdx}\""
15
+ "size": "size-limit"
19
16
  },
20
17
  "files": [
21
18
  "dist",
@@ -26,18 +23,13 @@
26
23
  "setupFilesAfterEnv": [
27
24
  "./__tests__/setupTests.ts"
28
25
  ],
26
+ "modulePathIgnorePatterns": [
27
+ "cypress"
28
+ ],
29
29
  "coveragePathIgnorePatterns": [
30
30
  "src/lib"
31
31
  ]
32
32
  },
33
- "prettier": {
34
- "singleQuote": true
35
- },
36
- "husky": {
37
- "hooks": {
38
- "pre-commit": "tsdx lint"
39
- }
40
- },
41
33
  "keywords": [
42
34
  "react",
43
35
  "responsive",
@@ -55,16 +47,17 @@
55
47
  "size-limit": [
56
48
  {
57
49
  "path": "dist/react-responsive-modal.cjs.production.min.js",
58
- "limit": "3.1 KB"
50
+ "limit": "4.1 KB"
59
51
  },
60
52
  {
61
53
  "path": "dist/react-responsive-modal.esm.js",
62
- "limit": "3.1 KB"
54
+ "limit": "4.1 KB"
63
55
  }
64
56
  ],
65
57
  "dependencies": {
66
- "classnames": "^2.2.6",
67
- "no-scroll": "^2.1.1"
58
+ "@bedrock-layout/use-forwarded-ref": "^1.1.4",
59
+ "body-scroll-lock": "^3.1.5",
60
+ "classnames": "^2.2.6"
68
61
  },
69
62
  "peerDependencies": {
70
63
  "react": "^16.8.0 || ^17",
@@ -72,21 +65,19 @@
72
65
  },
73
66
  "devDependencies": {
74
67
  "@size-limit/preset-small-lib": "4.7.0",
75
- "@testing-library/jest-dom": "5.11.5",
76
- "@testing-library/react": "11.1.1",
68
+ "@testing-library/jest-dom": "5.11.6",
69
+ "@testing-library/react": "11.1.2",
70
+ "@types/body-scroll-lock": "2.6.1",
77
71
  "@types/classnames": "2.2.11",
78
- "@types/no-scroll": "2.1.0",
79
- "@types/node": "14.14.6",
72
+ "@types/node": "14.14.7",
80
73
  "@types/react": "16.9.56",
81
74
  "@types/react-dom": "16.9.9",
82
- "@types/react-transition-group": "4.4.0",
83
- "docz": "2.3.1",
84
- "gatsby": "2.23.11",
85
- "gatsby-theme-docz": "2.3.1",
75
+ "babel-jest": "26.6.3",
76
+ "cypress": "5.6.0",
86
77
  "husky": "4.3.0",
87
78
  "prettier": "2.1.2",
88
- "react": "16.14.0",
89
- "react-dom": "16.14.0",
79
+ "react": "17.0.1",
80
+ "react-dom": "17.0.1",
90
81
  "size-limit": "4.7.0",
91
82
  "tsdx": "0.14.1",
92
83
  "tslib": "2.0.3",
package/src/CloseIcon.tsx CHANGED
@@ -15,7 +15,7 @@ interface CloseIconProps {
15
15
  classes: {
16
16
  closeButton?: string;
17
17
  };
18
- onClickCloseIcon: () => void;
18
+ onClick: () => void;
19
19
  }
20
20
 
21
21
  const CloseIcon = ({
@@ -24,13 +24,13 @@ const CloseIcon = ({
24
24
  styles,
25
25
  id,
26
26
  closeIcon,
27
- onClickCloseIcon,
27
+ onClick,
28
28
  }: CloseIconProps) => (
29
29
  <button
30
30
  id={id}
31
31
  className={cx(classes.closeButton, classNames?.closeButton)}
32
32
  style={styles?.closeButton}
33
- onClick={onClickCloseIcon}
33
+ onClick={onClick}
34
34
  data-testid="close-button"
35
35
  >
36
36
  {closeIcon ? (
@@ -39,7 +39,6 @@ const CloseIcon = ({
39
39
  <svg
40
40
  className={classNames?.closeIcon}
41
41
  style={styles?.closeIcon}
42
- xmlns="http://www.w3.org/2000/svg"
43
42
  width={28}
44
43
  height={28}
45
44
  viewBox="0 0 36 36"
package/src/FocusTrap.tsx CHANGED
@@ -8,9 +8,10 @@ import {
8
8
 
9
9
  interface FocusTrapProps {
10
10
  container?: React.RefObject<HTMLElement> | null;
11
+ initialFocusRef?: React.RefObject<HTMLElement>;
11
12
  }
12
13
 
13
- export const FocusTrap = ({ container }: FocusTrapProps) => {
14
+ export const FocusTrap = ({ container, initialFocusRef }: FocusTrapProps) => {
14
15
  const refLastFocus = useRef<HTMLElement | null>();
15
16
  /**
16
17
  * Handle focus lock on the modal
@@ -27,8 +28,7 @@ export const FocusTrap = ({ container }: FocusTrapProps) => {
27
28
  }
28
29
  // On mount we focus on the first focusable element in the modal if there is one
29
30
  if (isBrowser && container?.current) {
30
- const allTabbingElements = getAllTabbingElements(container.current);
31
- if (allTabbingElements[0]) {
31
+ const savePreviousFocus = () => {
32
32
  // First we save the last focused element
33
33
  // only if it's a focusable element
34
34
  if (
@@ -38,7 +38,20 @@ export const FocusTrap = ({ container }: FocusTrapProps) => {
38
38
  ) {
39
39
  refLastFocus.current = document.activeElement as HTMLElement;
40
40
  }
41
- allTabbingElements[0].focus();
41
+ };
42
+
43
+ if (initialFocusRef) {
44
+ savePreviousFocus();
45
+ // We need to schedule focusing on a next frame - this allows to focus on the modal root
46
+ requestAnimationFrame(() => {
47
+ initialFocusRef.current?.focus();
48
+ });
49
+ } else {
50
+ const allTabbingElements = getAllTabbingElements(container.current);
51
+ if (allTabbingElements[0]) {
52
+ savePreviousFocus();
53
+ allTabbingElements[0].focus();
54
+ }
42
55
  }
43
56
  }
44
57
  return () => {
@@ -48,7 +61,7 @@ export const FocusTrap = ({ container }: FocusTrapProps) => {
48
61
  refLastFocus.current?.focus();
49
62
  }
50
63
  };
51
- }, [container]);
64
+ }, [container, initialFocusRef]);
52
65
 
53
66
  return null;
54
67
  };
package/src/index.tsx CHANGED
@@ -1,18 +1,24 @@
1
- import React, { useEffect, useState, useRef } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import ReactDom from 'react-dom';
3
3
  import cx from 'classnames';
4
4
  import CloseIcon from './CloseIcon';
5
5
  import { FocusTrap } from './FocusTrap';
6
- import modalManager from './modalManager';
7
- import { isBrowser, blockNoScroll, unblockNoScroll } from './utils';
6
+ import { modalManager, useModalManager } from './modalManager';
7
+ import { useScrollLock } from './useScrollLock';
8
+ import { isBrowser } from './utils';
9
+ import useForwardedRef from '@bedrock-layout/use-forwarded-ref';
8
10
 
9
11
  const classes = {
12
+ root: 'react-responsive-modal-root',
10
13
  overlay: 'react-responsive-modal-overlay',
14
+ overlayAnimationIn: 'react-responsive-modal-overlay-in',
15
+ overlayAnimationOut: 'react-responsive-modal-overlay-out',
16
+ modalContainer: 'react-responsive-modal-container',
17
+ modalContainerCenter: 'react-responsive-modal-containerCenter',
11
18
  modal: 'react-responsive-modal-modal',
12
- modalCenter: 'react-responsive-modal-modalCenter',
19
+ modalAnimationIn: 'react-responsive-modal-modal-in',
20
+ modalAnimationOut: 'react-responsive-modal-modal-out',
13
21
  closeButton: 'react-responsive-modal-closeButton',
14
- animationIn: 'react-responsive-modal-fadeIn',
15
- animationOut: 'react-responsive-modal-fadeOut',
16
22
  };
17
23
 
18
24
  export interface ModalProps {
@@ -46,6 +52,8 @@ export interface ModalProps {
46
52
  blockScroll?: boolean;
47
53
  /**
48
54
  * Show the close icon.
55
+ *
56
+ * Default to true.
49
57
  */
50
58
  showCloseIcon?: boolean;
51
59
  /**
@@ -62,28 +70,40 @@ export interface ModalProps {
62
70
  * Default to true.
63
71
  */
64
72
  focusTrapped?: boolean;
73
+ /**
74
+ * Element to focus when focus trap is used.
75
+ *
76
+ * Default to undefined.
77
+ */
78
+ initialFocusRef?: React.RefObject<HTMLElement>;
65
79
  /**
66
80
  * You can specify a container prop which should be of type `Element`.
67
81
  * The portal will be rendered inside that element.
68
82
  * The default behavior will create a div node and render it at the at the end of document.body.
69
83
  */
70
- container?: Element;
84
+ container?: Element | null;
71
85
  /**
72
86
  * An object containing classNames to style the modal.
73
87
  */
74
88
  classNames?: {
89
+ root?: string;
75
90
  overlay?: string;
91
+ overlayAnimationIn?: string;
92
+ overlayAnimationOut?: string;
93
+ modalContainer?: string;
76
94
  modal?: string;
95
+ modalAnimationIn?: string;
96
+ modalAnimationOut?: string;
77
97
  closeButton?: string;
78
98
  closeIcon?: string;
79
- animationIn?: string;
80
- animationOut?: string;
81
99
  };
82
100
  /**
83
101
  * An object containing the styles objects to style the modal.
84
102
  */
85
103
  styles?: {
104
+ root?: React.CSSProperties;
86
105
  overlay?: React.CSSProperties;
106
+ modalContainer?: React.CSSProperties;
87
107
  modal?: React.CSSProperties;
88
108
  closeButton?: React.CSSProperties;
89
109
  closeIcon?: React.CSSProperties;
@@ -91,7 +111,7 @@ export interface ModalProps {
91
111
  /**
92
112
  * Animation duration in milliseconds.
93
113
  *
94
- * Default to 500.
114
+ * Default to 300.
95
115
  */
96
116
  animationDuration?: number;
97
117
  /**
@@ -108,6 +128,10 @@ export interface ModalProps {
108
128
  * ARIA description for modal
109
129
  */
110
130
  ariaDescribedby?: string;
131
+ /**
132
+ * Avoid unpleasant flickering effect when body overflow is hidden. For more information see https://www.npmjs.com/package/body-scroll-lock
133
+ */
134
+ reserveScrollBarGap?: boolean;
111
135
  /**
112
136
  * id attribute for modal
113
137
  */
@@ -133,197 +157,223 @@ export interface ModalProps {
133
157
  children?: React.ReactNode;
134
158
  }
135
159
 
136
- export const Modal = ({
137
- open,
138
- center,
139
- blockScroll = true,
140
- closeOnEsc = true,
141
- closeOnOverlayClick = true,
142
- container,
143
- showCloseIcon = true,
144
- closeIconId,
145
- closeIcon,
146
- focusTrapped = true,
147
- animationDuration = 500,
148
- classNames,
149
- styles,
150
- role = 'dialog',
151
- ariaDescribedby,
152
- ariaLabelledby,
153
- modalId,
154
- onClose,
155
- onEscKeyDown,
156
- onOverlayClick,
157
- onAnimationEnd,
158
- children,
159
- }: ModalProps) => {
160
- const refModal = useRef<HTMLDivElement>(null);
161
- const refShouldClose = useRef<boolean | null>(null);
162
- const refContainer = useRef<HTMLDivElement | null>(null);
163
- // Lazily create the ref instance
164
- // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
165
- if (refContainer.current === null && isBrowser) {
166
- refContainer.current = document.createElement('div');
167
- }
160
+ export const Modal = React.forwardRef(
161
+ (
162
+ {
163
+ open,
164
+ center,
165
+ blockScroll = true,
166
+ closeOnEsc = true,
167
+ closeOnOverlayClick = true,
168
+ container,
169
+ showCloseIcon = true,
170
+ closeIconId,
171
+ closeIcon,
172
+ focusTrapped = true,
173
+ initialFocusRef = undefined,
174
+ animationDuration = 300,
175
+ classNames,
176
+ styles,
177
+ role = 'dialog',
178
+ ariaDescribedby,
179
+ ariaLabelledby,
180
+ modalId,
181
+ onClose,
182
+ onEscKeyDown,
183
+ onOverlayClick,
184
+ onAnimationEnd,
185
+ children,
186
+ reserveScrollBarGap,
187
+ }: ModalProps,
188
+ ref: React.ForwardedRef<HTMLDivElement>
189
+ ) => {
190
+ const refDialog = useForwardedRef(ref);
191
+ const refModal = useRef<HTMLDivElement>(null);
192
+ const refShouldClose = useRef<boolean | null>(null);
193
+ const refContainer = useRef<HTMLDivElement | null>(null);
194
+ // Lazily create the ref instance
195
+ // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
196
+ if (refContainer.current === null && isBrowser) {
197
+ refContainer.current = document.createElement('div');
198
+ }
168
199
 
169
- // The value should be false for srr, that way when the component is hydrated client side,
170
- // it will match the server rendered content
171
- const [showPortal, setShowPortal] = useState(false);
200
+ // The value should be false for srr, that way when the component is hydrated client side,
201
+ // it will match the server rendered content
202
+ const [showPortal, setShowPortal] = useState(false);
172
203
 
173
- const handleOpen = () => {
174
- modalManager.add(refContainer.current!, blockScroll);
175
- if (blockScroll) {
176
- blockNoScroll();
177
- }
178
- if (
179
- refContainer.current &&
180
- !container &&
181
- !document.body.contains(refContainer.current)
182
- ) {
183
- document.body.appendChild(refContainer.current);
184
- }
185
- document.addEventListener('keydown', handleKeydown);
186
- };
204
+ // Hook used to manage multiple modals opened at the same time
205
+ useModalManager(refModal, open);
187
206
 
188
- const handleClose = () => {
189
- modalManager.remove(refContainer.current!);
190
- if (blockScroll) {
191
- unblockNoScroll();
192
- }
193
- if (
194
- refContainer.current &&
195
- !container &&
196
- document.body.contains(refContainer.current)
197
- ) {
198
- document.body.removeChild(refContainer.current);
199
- }
200
- document.removeEventListener('keydown', handleKeydown);
201
- };
207
+ // Hook used to manage the scroll
208
+ useScrollLock(refModal, open, showPortal, blockScroll, reserveScrollBarGap);
202
209
 
203
- const handleKeydown = (event: KeyboardEvent) => {
204
- // Only the last modal need to be escaped when pressing the esc key
205
- if (
206
- event.keyCode !== 27 ||
207
- !modalManager.isTopModal(refContainer.current!)
208
- ) {
209
- return;
210
- }
210
+ const handleOpen = () => {
211
+ if (
212
+ refContainer.current &&
213
+ !container &&
214
+ !document.body.contains(refContainer.current)
215
+ ) {
216
+ document.body.appendChild(refContainer.current);
217
+ }
211
218
 
212
- onEscKeyDown?.(event);
219
+ document.addEventListener('keydown', handleKeydown);
220
+ };
213
221
 
214
- if (closeOnEsc) {
215
- onClose();
216
- }
217
- };
222
+ const handleClose = () => {
223
+ if (
224
+ refContainer.current &&
225
+ !container &&
226
+ document.body.contains(refContainer.current)
227
+ ) {
228
+ document.body.removeChild(refContainer.current);
229
+ }
230
+ document.removeEventListener('keydown', handleKeydown);
231
+ };
232
+
233
+ const handleKeydown = (event: KeyboardEvent) => {
234
+ // Only the last modal need to be escaped when pressing the esc key
235
+ if (event.keyCode !== 27 || !modalManager.isTopModal(refModal)) {
236
+ return;
237
+ }
238
+
239
+ onEscKeyDown?.(event);
218
240
 
219
- useEffect(() => {
220
- return () => {
221
- // When the component is unmounted directly we want to unblock the scroll
222
- if (showPortal) {
223
- handleClose();
241
+ if (closeOnEsc) {
242
+ onClose();
224
243
  }
225
244
  };
226
- }, [showPortal]);
227
245
 
228
- useEffect(() => {
229
- // If the open prop is changing, we need to open the modal
230
- // This is also called on the first render if the open prop is true when the modal is created
231
- if (open && !showPortal) {
232
- setShowPortal(true);
233
- handleOpen();
234
- }
235
- }, [open]);
246
+ useEffect(() => {
247
+ return () => {
248
+ if (showPortal) {
249
+ // When the modal is closed or removed directly, cleanup the listeners
250
+ handleClose();
251
+ }
252
+ };
253
+ }, [showPortal]);
236
254
 
237
- const handleClickOverlay = (
238
- event: React.MouseEvent<HTMLDivElement, MouseEvent>
239
- ) => {
240
- if (refShouldClose.current === null) {
241
- refShouldClose.current = true;
242
- }
255
+ useEffect(() => {
256
+ // If the open prop is changing, we need to open the modal
257
+ // This is also called on the first render if the open prop is true when the modal is created
258
+ if (open && !showPortal) {
259
+ setShowPortal(true);
260
+ handleOpen();
261
+ }
262
+ }, [open]);
243
263
 
244
- if (!refShouldClose.current) {
245
- refShouldClose.current = null;
246
- return;
247
- }
264
+ const handleClickOverlay = (
265
+ event: React.MouseEvent<HTMLDivElement, MouseEvent>
266
+ ) => {
267
+ if (refShouldClose.current === null) {
268
+ refShouldClose.current = true;
269
+ }
248
270
 
249
- onOverlayClick?.(event);
271
+ if (!refShouldClose.current) {
272
+ refShouldClose.current = null;
273
+ return;
274
+ }
250
275
 
251
- if (closeOnOverlayClick) {
252
- onClose();
253
- }
276
+ onOverlayClick?.(event);
254
277
 
255
- refShouldClose.current = null;
256
- };
278
+ if (closeOnOverlayClick) {
279
+ onClose();
280
+ }
257
281
 
258
- const handleModalEvent = () => {
259
- refShouldClose.current = false;
260
- };
282
+ refShouldClose.current = null;
283
+ };
261
284
 
262
- const handleClickCloseIcon = () => {
263
- onClose();
264
- };
285
+ const handleModalEvent = () => {
286
+ refShouldClose.current = false;
287
+ };
265
288
 
266
- const handleAnimationEnd = () => {
267
- if (!open) {
268
- setShowPortal(false);
269
- }
289
+ const handleAnimationEnd = () => {
290
+ if (!open) {
291
+ setShowPortal(false);
292
+ }
270
293
 
271
- onAnimationEnd?.();
272
- };
294
+ onAnimationEnd?.();
295
+ };
296
+
297
+ const containerModal = container || refContainer.current;
273
298
 
274
- const containerModal = container || refContainer.current;
299
+ const overlayAnimation = open
300
+ ? classNames?.overlayAnimationIn ?? classes.overlayAnimationIn
301
+ : classNames?.overlayAnimationOut ?? classes.overlayAnimationOut;
275
302
 
276
- return showPortal && containerModal
277
- ? ReactDom.createPortal(
278
- <div
279
- style={{
280
- animation: `${
281
- open
282
- ? classNames?.animationIn ?? classes.animationIn
283
- : classNames?.animationOut ?? classes.animationOut
284
- } ${animationDuration}ms`,
285
- ...styles?.overlay,
286
- }}
287
- className={cx(classes.overlay, classNames?.overlay)}
288
- onClick={handleClickOverlay}
289
- onAnimationEnd={handleAnimationEnd}
290
- data-testid="overlay"
291
- >
303
+ const modalAnimation = open
304
+ ? classNames?.modalAnimationIn ?? classes.modalAnimationIn
305
+ : classNames?.modalAnimationOut ?? classes.modalAnimationOut;
306
+
307
+ return showPortal && containerModal
308
+ ? ReactDom.createPortal(
292
309
  <div
293
- ref={refModal}
294
- className={cx(
295
- classes.modal,
296
- center && classes.modalCenter,
297
- classNames?.modal
298
- )}
299
- style={styles?.modal}
300
- onMouseDown={handleModalEvent}
301
- onMouseUp={handleModalEvent}
302
- onClick={handleModalEvent}
303
- id={modalId}
304
- role={role}
305
- aria-modal="true"
306
- aria-labelledby={ariaLabelledby}
307
- aria-describedby={ariaDescribedby}
308
- data-testid="modal"
310
+ className={cx(classes.root, classNames?.root)}
311
+ style={styles?.root}
312
+ data-testid="root"
309
313
  >
310
- {focusTrapped && <FocusTrap container={refModal} />}
311
- {children}
312
- {showCloseIcon && (
313
- <CloseIcon
314
- classes={classes}
315
- classNames={classNames}
316
- styles={styles}
317
- closeIcon={closeIcon}
318
- onClickCloseIcon={handleClickCloseIcon}
319
- id={closeIconId}
320
- />
321
- )}
322
- </div>
323
- </div>,
324
- containerModal
325
- )
326
- : null;
327
- };
314
+ <div
315
+ className={cx(classes.overlay, classNames?.overlay)}
316
+ data-testid="overlay"
317
+ aria-hidden={true}
318
+ style={{
319
+ animation: `${overlayAnimation} ${animationDuration}ms`,
320
+ ...styles?.overlay,
321
+ }}
322
+ />
323
+ <div
324
+ ref={refModal}
325
+ className={cx(
326
+ classes.modalContainer,
327
+ center && classes.modalContainerCenter,
328
+ classNames?.modalContainer
329
+ )}
330
+ style={styles?.modalContainer}
331
+ data-testid="modal-container"
332
+ onClick={handleClickOverlay}
333
+ >
334
+ <div
335
+ ref={refDialog}
336
+ className={cx(classes.modal, classNames?.modal)}
337
+ style={{
338
+ animation: `${modalAnimation} ${animationDuration}ms`,
339
+ ...styles?.modal,
340
+ }}
341
+ onMouseDown={handleModalEvent}
342
+ onMouseUp={handleModalEvent}
343
+ onClick={handleModalEvent}
344
+ onAnimationEnd={handleAnimationEnd}
345
+ id={modalId}
346
+ role={role}
347
+ aria-modal="true"
348
+ aria-labelledby={ariaLabelledby}
349
+ aria-describedby={ariaDescribedby}
350
+ data-testid="modal"
351
+ tabIndex={-1}
352
+ >
353
+ {focusTrapped && (
354
+ <FocusTrap
355
+ container={refDialog}
356
+ initialFocusRef={initialFocusRef}
357
+ />
358
+ )}
359
+ {children}
360
+ {showCloseIcon && (
361
+ <CloseIcon
362
+ classes={classes}
363
+ classNames={classNames}
364
+ styles={styles}
365
+ closeIcon={closeIcon}
366
+ onClick={onClose}
367
+ id={closeIconId}
368
+ />
369
+ )}
370
+ </div>
371
+ </div>
372
+ </div>,
373
+ containerModal
374
+ )
375
+ : null;
376
+ }
377
+ );
328
378
 
329
379
  export default Modal;