-
Notifications
You must be signed in to change notification settings - Fork 546
/
Copy pathModal.tsx
190 lines (167 loc) · 5.43 KB
/
Modal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import React, { useEffect, useRef, useLayoutEffect } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button/Button";
import Icon from "components/Icon/Icon";
const baseClass = "modal";
type ModalWidth = "medium" | "large" | "xlarge" | "auto";
export interface IModalProps {
title: string | JSX.Element;
children: JSX.Element;
onExit: () => void;
onEnter?: () => void;
/** medium 650px, large 800px, xlarge 850px, auto auto-width
* @default "medium"
*/
width?: ModalWidth;
/** isHidden can be set true to hide the modal when opening another modal
* @default false
*/
isHidden?: boolean;
/** isLoading can be set true to enable targeting elements by loading state
* @default false
*/
isLoading?: boolean;
/** `isContentDisabled` can be set to true to display the modal content as disabled.
* At the moment this will place an overlay over the modal content and make it
* unclickable. The top right will not be disabled and will still be clickable.
*
* @default false
*/
isContentDisabled?: boolean;
/** `disableClosingModal` can be set to disable the users ability to manually
* close the modal.
* @default false
* */
disableClosingModal?: boolean;
className?: string;
}
const Modal = ({
title,
children,
onExit,
onEnter,
width = "medium",
isHidden = false,
isLoading = false,
isContentDisabled = false,
disableClosingModal = false,
className,
}: IModalProps): JSX.Element => {
const contentRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
const isClosingRef = useRef(false);
// This returns focus to the previous active element before opening the modal
useLayoutEffect(() => {
previousActiveElement.current = document.activeElement as HTMLElement;
}, []);
useEffect(() => {
const observer = new MutationObserver(() => {
if (
!isClosingRef.current &&
!document.body.contains(contentRef.current)
) {
isClosingRef.current = true;
if (previousActiveElement.current) {
previousActiveElement.current.focus();
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
return () => {
observer.disconnect();
if (previousActiveElement.current) {
previousActiveElement.current.focus();
}
};
}, []);
/** Allows keyboard accessibility to modals -- Because of loading,
* we cannot have a global fix to access focusable elements *within*
* children, but we can access the close button on the Modal */
useEffect(() => {
previousActiveElement.current = document.activeElement as HTMLElement;
// This just grabs the x button, left it robust incase we use it to grab
// a different focusable element in the future
if (contentRef.current) {
const focusableElements = contentRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
(focusableElements[0] as HTMLElement).focus();
}
}
return () => {
if (previousActiveElement.current) {
previousActiveElement.current.focus();
}
};
}, []);
useEffect(() => {
const closeWithEscapeKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onExit();
}
};
if (!disableClosingModal) {
document.addEventListener("keydown", closeWithEscapeKey);
}
return () => {
if (!disableClosingModal) {
document.removeEventListener("keydown", closeWithEscapeKey);
}
};
}, [disableClosingModal, onExit]);
useEffect(() => {
if (onEnter) {
const closeOrSaveWithEnterKey = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
onEnter();
}
};
document.addEventListener("keydown", closeOrSaveWithEnterKey);
return () => {
document.removeEventListener("keydown", closeOrSaveWithEnterKey);
};
}
}, [onEnter]);
const backgroundClasses = classnames(`${baseClass}__background`, {
[`${baseClass}__hidden`]: isHidden,
});
const modalContainerClasses = classnames(
className,
`${baseClass}__modal_container`,
`${baseClass}__modal_container__${width}`,
{
[`${className}__loading`]: isLoading,
}
);
const contentWrapperClasses = classnames(`${baseClass}__content-wrapper`, {
[`${baseClass}__content-wrapper-disabled`]: isContentDisabled,
});
const contentClasses = classnames(`${baseClass}__content`, {
[`${baseClass}__content-disabled`]: isContentDisabled,
});
return (
<div ref={contentRef} className={backgroundClasses}>
<div className={modalContainerClasses}>
<div className={`${baseClass}__header`}>
<span>{title}</span>
{!disableClosingModal && (
<div className={`${baseClass}__ex`}>
<Button variant="unstyled" onClick={onExit}>
<Icon name="close" color="core-fleet-black" size="medium" />
</Button>
</div>
)}
</div>
<div className={contentWrapperClasses}>
{isContentDisabled && (
<div className={`${baseClass}__disabled-overlay`} />
)}
<div className={contentClasses}>{children}</div>
</div>
</div>
</div>
);
};
export default Modal;