react-responsive-modal 4.0.1 → 5.0.3

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.
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import cx from 'classnames';
3
+
4
+ interface CloseIconProps {
5
+ id?: string;
6
+ closeIcon?: React.ReactNode;
7
+ styles?: {
8
+ closeButton?: React.CSSProperties;
9
+ closeIcon?: React.CSSProperties;
10
+ };
11
+ classNames?: {
12
+ closeButton?: string;
13
+ closeIcon?: string;
14
+ };
15
+ classes: {
16
+ closeButton?: string;
17
+ };
18
+ onClickCloseIcon: () => void;
19
+ }
20
+
21
+ const CloseIcon = ({
22
+ classes,
23
+ classNames,
24
+ styles,
25
+ id,
26
+ closeIcon,
27
+ onClickCloseIcon,
28
+ }: CloseIconProps) => (
29
+ <button
30
+ id={id}
31
+ className={cx(classes.closeButton, classNames?.closeButton)}
32
+ style={styles?.closeButton}
33
+ onClick={onClickCloseIcon}
34
+ data-testid="close-button"
35
+ >
36
+ {closeIcon ? (
37
+ closeIcon
38
+ ) : (
39
+ <svg
40
+ className={classNames?.closeIcon}
41
+ style={styles?.closeIcon}
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ width={28}
44
+ height={28}
45
+ viewBox="0 0 36 36"
46
+ data-testid="close-icon"
47
+ >
48
+ <path d="M28.5 9.62L26.38 7.5 18 15.88 9.62 7.5 7.5 9.62 15.88 18 7.5 26.38l2.12 2.12L18 20.12l8.38 8.38 2.12-2.12L20.12 18z" />
49
+ </svg>
50
+ )}
51
+ </button>
52
+ );
53
+
54
+ export default CloseIcon;
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { isBrowser } from './utils';
3
+ import {
4
+ tabTrappingKey,
5
+ candidateSelectors,
6
+ getAllTabbingElements,
7
+ } from './focusTrapJs';
8
+
9
+ interface FocusTrapProps {
10
+ container?: React.RefObject<HTMLElement> | null;
11
+ }
12
+
13
+ export const FocusTrap = ({ container }: FocusTrapProps) => {
14
+ const refLastFocus = useRef<HTMLElement | null>();
15
+ /**
16
+ * Handle focus lock on the modal
17
+ */
18
+ useEffect(() => {
19
+ const handleKeyEvent = (event: KeyboardEvent) => {
20
+ if (container?.current) {
21
+ tabTrappingKey(event, container.current);
22
+ }
23
+ };
24
+
25
+ if (isBrowser) {
26
+ document.addEventListener('keydown', handleKeyEvent);
27
+ }
28
+ // On mount we focus on the first focusable element in the modal if there is one
29
+ if (isBrowser && container?.current) {
30
+ const allTabbingElements = getAllTabbingElements(container.current);
31
+ if (allTabbingElements[0]) {
32
+ // First we save the last focused element
33
+ // only if it's a focusable element
34
+ if (
35
+ candidateSelectors.findIndex((selector) =>
36
+ document.activeElement?.matches(selector)
37
+ ) !== -1
38
+ ) {
39
+ refLastFocus.current = document.activeElement as HTMLElement;
40
+ }
41
+ allTabbingElements[0].focus();
42
+ }
43
+ }
44
+ return () => {
45
+ if (isBrowser) {
46
+ document.removeEventListener('keydown', handleKeyEvent);
47
+ // On unmount we restore the focus to the last focused element
48
+ refLastFocus.current?.focus();
49
+ }
50
+ };
51
+ }, [container]);
52
+
53
+ return null;
54
+ };
@@ -0,0 +1,79 @@
1
+ // https://github.com/alexandrzavalii/focus-trap-js/blob/master/src/index.js v1.0.9
2
+
3
+ export const candidateSelectors = [
4
+ 'input',
5
+ 'select',
6
+ 'textarea',
7
+ 'a[href]',
8
+ 'button',
9
+ '[tabindex]',
10
+ 'audio[controls]',
11
+ 'video[controls]',
12
+ '[contenteditable]:not([contenteditable="false"])',
13
+ ];
14
+
15
+ function isHidden(node: any) {
16
+ // offsetParent being null will allow detecting cases where an element is invisible or inside an invisible element,
17
+ // as long as the element does not use position: fixed. For them, their visibility has to be checked directly as well.
18
+ return (
19
+ node.offsetParent === null || getComputedStyle(node).visibility === 'hidden'
20
+ );
21
+ }
22
+
23
+ export function getAllTabbingElements(parentElem: any) {
24
+ var tabbableNodes = parentElem.querySelectorAll(candidateSelectors.join(','));
25
+ var onlyTabbable = [];
26
+ for (var i = 0; i < tabbableNodes.length; i++) {
27
+ var node = tabbableNodes[i];
28
+ if (!node.disabled && getTabindex(node) > -1 && !isHidden(node)) {
29
+ onlyTabbable.push(node);
30
+ }
31
+ }
32
+ return onlyTabbable;
33
+ }
34
+
35
+ export function tabTrappingKey(event: any, parentElem: any) {
36
+ // check if current event keyCode is tab
37
+ if (!event || event.key !== 'Tab') return;
38
+
39
+ if (!parentElem || !parentElem.contains) {
40
+ if (process && process.env.NODE_ENV === 'development') {
41
+ console.warn('focus-trap-js: parent element is not defined');
42
+ }
43
+ return false;
44
+ }
45
+
46
+ if (!parentElem.contains(event.target)) {
47
+ return false;
48
+ }
49
+
50
+ var allTabbingElements = getAllTabbingElements(parentElem);
51
+ var firstFocusableElement = allTabbingElements[0];
52
+ var lastFocusableElement = allTabbingElements[allTabbingElements.length - 1];
53
+
54
+ if (event.shiftKey && event.target === firstFocusableElement) {
55
+ lastFocusableElement.focus();
56
+ event.preventDefault();
57
+ return true;
58
+ } else if (!event.shiftKey && event.target === lastFocusableElement) {
59
+ firstFocusableElement.focus();
60
+ event.preventDefault();
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function getTabindex(node: any) {
67
+ var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);
68
+
69
+ if (!isNaN(tabindexAttr)) return tabindexAttr;
70
+ // Browsers do not return tabIndex correctly for contentEditable nodes;
71
+ // so if they don't have a tabindex attribute specifically set, assume it's 0.
72
+
73
+ if (isContentEditable(node)) return 0;
74
+ return node.tabIndex;
75
+ }
76
+
77
+ function isContentEditable(node: any) {
78
+ return node.getAttribute('contentEditable');
79
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,339 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import ReactDom from 'react-dom';
3
+ import cx from 'classnames';
4
+ import CloseIcon from './CloseIcon';
5
+ import { FocusTrap } from './FocusTrap';
6
+ import modalManager from './modalManager';
7
+ import { isBrowser, blockNoScroll, unblockNoScroll } from './utils';
8
+
9
+ const classes = {
10
+ overlay: 'react-responsive-modal-overlay',
11
+ modal: 'react-responsive-modal-modal',
12
+ modalCenter: 'react-responsive-modal-modalCenter',
13
+ closeButton: 'react-responsive-modal-closeButton',
14
+ animationIn: 'react-responsive-modal-fadeIn',
15
+ animationOut: 'react-responsive-modal-fadeOut',
16
+ };
17
+
18
+ interface ModalProps {
19
+ /**
20
+ * Control if the modal is open or not.
21
+ */
22
+ open: boolean;
23
+ /**
24
+ * Should the dialog be centered.
25
+ *
26
+ * Default to false.
27
+ */
28
+ center?: boolean;
29
+ /**
30
+ * Is the modal closable when user press esc key.
31
+ *
32
+ * Default to true.
33
+ */
34
+ closeOnEsc?: boolean;
35
+ /**
36
+ * Is the modal closable when user click on overlay.
37
+ *
38
+ * Default to true.
39
+ */
40
+ closeOnOverlayClick?: boolean;
41
+ /**
42
+ * Whether to block scrolling when dialog is open.
43
+ *
44
+ * Default to true.
45
+ */
46
+ blockScroll?: boolean;
47
+ /**
48
+ * Show the close icon.
49
+ */
50
+ showCloseIcon?: boolean;
51
+ /**
52
+ * id attribute for the close icon button.
53
+ */
54
+ closeIconId?: string;
55
+ /**
56
+ * Custom icon to render (svg, img, etc...).
57
+ */
58
+ closeIcon?: React.ReactNode;
59
+ /**
60
+ * When the modal is open, trap focus within it.
61
+ *
62
+ * Default to true.
63
+ */
64
+ focusTrapped?: boolean;
65
+ /**
66
+ * You can specify a container prop which should be of type `Element`.
67
+ * The portal will be rendered inside that element.
68
+ * The default behavior will create a div node and render it at the at the end of document.body.
69
+ */
70
+ container?: Element;
71
+ /**
72
+ * An object containing classNames to style the modal.
73
+ */
74
+ classNames?: {
75
+ overlay?: string;
76
+ modal?: string;
77
+ closeButton?: string;
78
+ closeIcon?: string;
79
+ animationIn?: string;
80
+ animationOut?: string;
81
+ };
82
+ /**
83
+ * An object containing the styles objects to style the modal.
84
+ */
85
+ styles?: {
86
+ overlay?: React.CSSProperties;
87
+ modal?: React.CSSProperties;
88
+ closeButton?: React.CSSProperties;
89
+ closeIcon?: React.CSSProperties;
90
+ };
91
+ /**
92
+ * Animation duration in milliseconds.
93
+ *
94
+ * Default to 500.
95
+ */
96
+ animationDuration?: number;
97
+ /**
98
+ * ARIA role for modal
99
+ *
100
+ * Default to 'dialog'.
101
+ */
102
+ role?: string;
103
+ /**
104
+ * ARIA label for modal
105
+ */
106
+ ariaLabelledby?: string;
107
+ /**
108
+ * ARIA description for modal
109
+ */
110
+ ariaDescribedby?: string;
111
+ /**
112
+ * id attribute for modal
113
+ */
114
+ modalId?: string;
115
+ /**
116
+ * Callback fired when the Modal is requested to be closed by a click on the overlay or when user press esc key.
117
+ */
118
+ onClose: () => void;
119
+ /**
120
+ * Callback fired when the escape key is pressed.
121
+ */
122
+ onEscKeyDown?: (event: KeyboardEvent) => void;
123
+ /**
124
+ * Callback fired when the overlay is clicked.
125
+ */
126
+ onOverlayClick?: (
127
+ event: React.MouseEvent<HTMLDivElement, MouseEvent>
128
+ ) => void;
129
+ /**
130
+ * Callback fired when the Modal has exited and the animation is finished.
131
+ */
132
+ onAnimationEnd?: () => void;
133
+ children?: React.ReactNode;
134
+ }
135
+
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
+ }
168
+
169
+ const [showPortal, setShowPortal] = useState(open);
170
+
171
+ const handleOpen = () => {
172
+ modalManager.add(refContainer.current!, blockScroll);
173
+ if (blockScroll) {
174
+ blockNoScroll();
175
+ }
176
+ if (
177
+ refContainer.current &&
178
+ !container &&
179
+ !document.body.contains(refContainer.current)
180
+ ) {
181
+ document.body.appendChild(refContainer.current);
182
+ }
183
+ document.addEventListener('keydown', handleKeydown);
184
+ };
185
+
186
+ const handleClose = () => {
187
+ modalManager.remove(refContainer.current!);
188
+ if (blockScroll) {
189
+ unblockNoScroll();
190
+ }
191
+ if (
192
+ refContainer.current &&
193
+ !container &&
194
+ document.body.contains(refContainer.current)
195
+ ) {
196
+ document.body.removeChild(refContainer.current);
197
+ }
198
+ document.removeEventListener('keydown', handleKeydown);
199
+ };
200
+
201
+ const handleKeydown = (event: KeyboardEvent) => {
202
+ // Only the last modal need to be escaped when pressing the esc key
203
+ if (
204
+ event.keyCode !== 27 ||
205
+ !modalManager.isTopModal(refContainer.current!)
206
+ ) {
207
+ return;
208
+ }
209
+
210
+ if (onEscKeyDown) {
211
+ onEscKeyDown(event);
212
+ }
213
+
214
+ if (closeOnEsc) {
215
+ onClose();
216
+ }
217
+ };
218
+
219
+ useEffect(() => {
220
+ // When the modal is rendered first time we want to block the scroll
221
+ if (open) {
222
+ handleOpen();
223
+ }
224
+ return () => {
225
+ // When the component is unmounted directly we want to unblock the scroll
226
+ if (showPortal) {
227
+ handleClose();
228
+ }
229
+ };
230
+ }, []);
231
+
232
+ useEffect(() => {
233
+ // If the open prop is changing, we need to open the modal
234
+ if (open && !showPortal) {
235
+ setShowPortal(true);
236
+ handleOpen();
237
+ }
238
+ }, [open]);
239
+
240
+ const handleClickOverlay = (
241
+ event: React.MouseEvent<HTMLDivElement, MouseEvent>
242
+ ) => {
243
+ if (refShouldClose.current === null) {
244
+ refShouldClose.current = true;
245
+ }
246
+
247
+ if (!refShouldClose.current) {
248
+ refShouldClose.current = null;
249
+ return;
250
+ }
251
+
252
+ if (onOverlayClick) {
253
+ onOverlayClick(event);
254
+ }
255
+
256
+ if (closeOnOverlayClick) {
257
+ onClose();
258
+ }
259
+
260
+ refShouldClose.current = null;
261
+ };
262
+
263
+ const handleModalEvent = () => {
264
+ refShouldClose.current = false;
265
+ };
266
+
267
+ const handleClickCloseIcon = () => {
268
+ onClose();
269
+ };
270
+
271
+ const handleAnimationEnd = () => {
272
+ if (!open) {
273
+ setShowPortal(false);
274
+ handleClose();
275
+ }
276
+
277
+ if (blockScroll) {
278
+ unblockNoScroll();
279
+ }
280
+
281
+ if (onAnimationEnd) {
282
+ onAnimationEnd();
283
+ }
284
+ };
285
+
286
+ return showPortal
287
+ ? ReactDom.createPortal(
288
+ <div
289
+ style={{
290
+ animation: `${
291
+ open
292
+ ? classNames?.animationIn ?? classes.animationIn
293
+ : classNames?.animationOut ?? classes.animationOut
294
+ } ${animationDuration}ms`,
295
+ ...styles?.overlay,
296
+ }}
297
+ className={cx(classes.overlay, classNames?.overlay)}
298
+ onClick={handleClickOverlay}
299
+ onAnimationEnd={handleAnimationEnd}
300
+ data-testid="overlay"
301
+ >
302
+ <div
303
+ ref={refModal}
304
+ className={cx(
305
+ classes.modal,
306
+ center && classes.modalCenter,
307
+ classNames?.modal
308
+ )}
309
+ style={styles?.modal}
310
+ onMouseDown={handleModalEvent}
311
+ onMouseUp={handleModalEvent}
312
+ onClick={handleModalEvent}
313
+ id={modalId}
314
+ role={role}
315
+ aria-modal="true"
316
+ aria-labelledby={ariaLabelledby}
317
+ aria-describedby={ariaDescribedby}
318
+ data-testid="modal"
319
+ >
320
+ {focusTrapped && <FocusTrap container={refModal} />}
321
+ {children}
322
+ {showCloseIcon && (
323
+ <CloseIcon
324
+ classes={classes}
325
+ classNames={classNames}
326
+ styles={styles}
327
+ closeIcon={closeIcon}
328
+ onClickCloseIcon={handleClickCloseIcon}
329
+ id={closeIconId}
330
+ />
331
+ )}
332
+ </div>
333
+ </div>,
334
+ container || refContainer.current!
335
+ )
336
+ : null;
337
+ };
338
+
339
+ export default Modal;
@@ -0,0 +1,37 @@
1
+ const modals: { element: HTMLDivElement; blockScroll: boolean }[] = [];
2
+
3
+ /**
4
+ * Handle the order of the modals.
5
+ * Inspired by the material-ui implementation.
6
+ */
7
+ export default {
8
+ /**
9
+ * Return the modals array
10
+ */
11
+ modals: () => modals,
12
+
13
+ /**
14
+ * Register a new modal
15
+ */
16
+ add: (newModal: HTMLDivElement, blockScroll: boolean) => {
17
+ if (modals.findIndex((modal) => modal.element === newModal) === -1) {
18
+ modals.push({ element: newModal, blockScroll });
19
+ }
20
+ },
21
+
22
+ /**
23
+ * Remove a modal
24
+ */
25
+ remove: (oldModal: HTMLDivElement) => {
26
+ const index = modals.findIndex((modal) => modal.element === oldModal);
27
+ if (index !== -1) {
28
+ modals.splice(index, 1);
29
+ }
30
+ },
31
+
32
+ /**
33
+ * Check if the modal is the first one on the screen
34
+ */
35
+ isTopModal: (modal: HTMLDivElement) =>
36
+ !!modals.length && modals[modals.length - 1]?.element === modal,
37
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,17 @@
1
+ import noScroll from 'no-scroll';
2
+ import modalManager from './modalManager';
3
+
4
+ export const isBrowser = typeof window !== 'undefined';
5
+
6
+ export const blockNoScroll = () => {
7
+ noScroll.on();
8
+ };
9
+
10
+ export const unblockNoScroll = () => {
11
+ // Restore the scroll only if there is no modal on the screen
12
+ // We filter the modals that are not affecting the scroll
13
+ const modals = modalManager.modals().filter((modal) => modal.blockScroll);
14
+ if (modals.length === 0) {
15
+ noScroll.off();
16
+ }
17
+ };
@@ -1,4 +1,4 @@
1
- .overlay {
1
+ .react-responsive-modal-overlay {
2
2
  background: rgba(0, 0, 0, 0.75);
3
3
  display: flex;
4
4
  align-items: flex-start;
@@ -12,7 +12,8 @@
12
12
  z-index: 1000;
13
13
  padding: 1.2rem;
14
14
  }
15
- .modal {
15
+
16
+ .react-responsive-modal-modal {
16
17
  max-width: 800px;
17
18
  position: relative;
18
19
  padding: 1.2rem;
@@ -21,31 +22,36 @@
21
22
  box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.25);
22
23
  margin: 0 auto;
23
24
  }
24
- .modalCenter {
25
+
26
+ .react-responsive-modal-modalCenter {
25
27
  margin: auto;
26
28
  }
27
- .closeButton {
29
+
30
+ .react-responsive-modal-closeButton {
28
31
  position: absolute;
29
32
  top: 14px;
30
33
  right: 14px;
31
34
  border: none;
32
35
  padding: 0;
36
+ cursor: pointer;
33
37
  background-color: transparent;
34
38
  display: flex;
35
39
  }
36
- .closeIcon {
37
- }
38
- .transitionEnter {
39
- opacity: 0.01;
40
- }
41
- .transitionEnterActive {
42
- opacity: 1;
43
- transition: opacity 500ms cubic-bezier(0.23, 1, 0.32, 1);
44
- }
45
- .transitionExit {
46
- opacity: 1;
47
- }
48
- .transitionExitActive {
49
- opacity: 0.01;
50
- transition: opacity 500ms cubic-bezier(0.23, 1, 0.32, 1);
40
+
41
+ @keyframes react-responsive-modal-fadeIn {
42
+ 0% {
43
+ opacity: 0;
44
+ }
45
+ 100% {
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ @keyframes react-responsive-modal-fadeOut {
51
+ 0% {
52
+ opacity: 1;
53
+ }
54
+ 100% {
55
+ opacity: 0;
56
+ }
51
57
  }