UNPKG

13.7 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3
4/* eslint-disable @typescript-eslint/no-use-before-define */
5
6/* eslint-disable react/prop-types */
7import activeElement from 'dom-helpers/activeElement';
8import contains from 'dom-helpers/contains';
9import canUseDOM from 'dom-helpers/canUseDOM';
10import listen from 'dom-helpers/listen';
11import PropTypes from 'prop-types';
12import React, { useState, useRef, useCallback, useImperativeHandle, forwardRef, useEffect } from 'react';
13import ReactDOM from 'react-dom';
14import useMounted from '@restart/hooks/useMounted';
15import useWillUnmount from '@restart/hooks/useWillUnmount';
16import usePrevious from '@restart/hooks/usePrevious';
17import useEventCallback from '@restart/hooks/useEventCallback';
18import ModalManager from './ModalManager';
19import useWaitForDOMRef from './useWaitForDOMRef';
20var manager;
21
22function getManager() {
23 if (!manager) manager = new ModalManager();
24 return manager;
25}
26
27function useModalManager(provided) {
28 var modalManager = provided || getManager();
29 var modal = useRef({
30 dialog: null,
31 backdrop: null
32 });
33 return Object.assign(modal.current, {
34 add: function add(container, className) {
35 return modalManager.add(modal.current, container, className);
36 },
37 remove: function remove() {
38 return modalManager.remove(modal.current);
39 },
40 isTopModal: function isTopModal() {
41 return modalManager.isTopModal(modal.current);
42 },
43 setDialogRef: useCallback(function (ref) {
44 modal.current.dialog = ref;
45 }, []),
46 setBackdropRef: useCallback(function (ref) {
47 modal.current.backdrop = ref;
48 }, [])
49 });
50}
51
52var Modal = forwardRef(function (_ref, ref) {
53 var _ref$show = _ref.show,
54 show = _ref$show === void 0 ? false : _ref$show,
55 _ref$role = _ref.role,
56 role = _ref$role === void 0 ? 'dialog' : _ref$role,
57 className = _ref.className,
58 style = _ref.style,
59 children = _ref.children,
60 _ref$backdrop = _ref.backdrop,
61 backdrop = _ref$backdrop === void 0 ? true : _ref$backdrop,
62 _ref$keyboard = _ref.keyboard,
63 keyboard = _ref$keyboard === void 0 ? true : _ref$keyboard,
64 onBackdropClick = _ref.onBackdropClick,
65 onEscapeKeyDown = _ref.onEscapeKeyDown,
66 transition = _ref.transition,
67 backdropTransition = _ref.backdropTransition,
68 _ref$autoFocus = _ref.autoFocus,
69 autoFocus = _ref$autoFocus === void 0 ? true : _ref$autoFocus,
70 _ref$enforceFocus = _ref.enforceFocus,
71 enforceFocus = _ref$enforceFocus === void 0 ? true : _ref$enforceFocus,
72 _ref$restoreFocus = _ref.restoreFocus,
73 restoreFocus = _ref$restoreFocus === void 0 ? true : _ref$restoreFocus,
74 restoreFocusOptions = _ref.restoreFocusOptions,
75 renderDialog = _ref.renderDialog,
76 _ref$renderBackdrop = _ref.renderBackdrop,
77 renderBackdrop = _ref$renderBackdrop === void 0 ? function (props) {
78 return /*#__PURE__*/React.createElement("div", props);
79 } : _ref$renderBackdrop,
80 providedManager = _ref.manager,
81 containerRef = _ref.container,
82 containerClassName = _ref.containerClassName,
83 onShow = _ref.onShow,
84 _ref$onHide = _ref.onHide,
85 onHide = _ref$onHide === void 0 ? function () {} : _ref$onHide,
86 onExit = _ref.onExit,
87 onExited = _ref.onExited,
88 onExiting = _ref.onExiting,
89 onEnter = _ref.onEnter,
90 onEntering = _ref.onEntering,
91 onEntered = _ref.onEntered,
92 rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "containerClassName", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]);
93
94 var container = useWaitForDOMRef(containerRef);
95 var modal = useModalManager(providedManager);
96 var isMounted = useMounted();
97 var prevShow = usePrevious(show);
98
99 var _useState = useState(!show),
100 exited = _useState[0],
101 setExited = _useState[1];
102
103 var lastFocusRef = useRef(null);
104 useImperativeHandle(ref, function () {
105 return modal;
106 }, [modal]);
107
108 if (canUseDOM && !prevShow && show) {
109 lastFocusRef.current = activeElement();
110 }
111
112 if (!transition && !show && !exited) {
113 setExited(true);
114 } else if (show && exited) {
115 setExited(false);
116 }
117
118 var handleShow = useEventCallback(function () {
119 modal.add(container, containerClassName);
120 removeKeydownListenerRef.current = listen(document, 'keydown', handleDocumentKeyDown);
121 removeFocusListenerRef.current = listen(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted
122 // and so steals focus from it
123 function () {
124 return setTimeout(handleEnforceFocus);
125 }, true);
126
127 if (onShow) {
128 onShow();
129 } // autofocus after onShow to not trigger a focus event for previous
130 // modals before this one is shown.
131
132
133 if (autoFocus) {
134 var currentActiveElement = activeElement(document);
135
136 if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) {
137 lastFocusRef.current = currentActiveElement;
138 modal.dialog.focus();
139 }
140 }
141 });
142 var handleHide = useEventCallback(function () {
143 modal.remove();
144 removeKeydownListenerRef.current == null ? void 0 : removeKeydownListenerRef.current();
145 removeFocusListenerRef.current == null ? void 0 : removeFocusListenerRef.current();
146
147 if (restoreFocus) {
148 var _lastFocusRef$current;
149
150 // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917)
151 (_lastFocusRef$current = lastFocusRef.current) == null ? void 0 : _lastFocusRef$current.focus == null ? void 0 : _lastFocusRef$current.focus(restoreFocusOptions);
152 lastFocusRef.current = null;
153 }
154 }); // TODO: try and combine these effects: https://github.com/react-bootstrap/react-overlays/pull/794#discussion_r409954120
155 // Show logic when:
156 // - show is `true` _and_ `container` has resolved
157
158 useEffect(function () {
159 if (!show || !container) return;
160 handleShow();
161 }, [show, container,
162 /* should never change: */
163 handleShow]); // Hide cleanup logic when:
164 // - `exited` switches to true
165 // - component unmounts;
166
167 useEffect(function () {
168 if (!exited) return;
169 handleHide();
170 }, [exited, handleHide]);
171 useWillUnmount(function () {
172 handleHide();
173 }); // --------------------------------
174
175 var handleEnforceFocus = useEventCallback(function () {
176 if (!enforceFocus || !isMounted() || !modal.isTopModal()) {
177 return;
178 }
179
180 var currentActiveElement = activeElement();
181
182 if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) {
183 modal.dialog.focus();
184 }
185 });
186 var handleBackdropClick = useEventCallback(function (e) {
187 if (e.target !== e.currentTarget) {
188 return;
189 }
190
191 onBackdropClick == null ? void 0 : onBackdropClick(e);
192
193 if (backdrop === true) {
194 onHide();
195 }
196 });
197
198 var handleDocumentKeyDown = function handleDocumentKeyDown(e) {
199 if (keyboard && e.keyCode === 27 && modal.isTopModal()) {
200 onEscapeKeyDown == null ? void 0 : onEscapeKeyDown(e);
201 onHide();
202 }
203 };
204
205 var removeFocusListenerRef = useRef();
206 var removeKeydownListenerRef = useRef();
207
208 var handleHidden = function handleHidden() {
209 setExited(true);
210
211 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
212 args[_key] = arguments[_key];
213 }
214
215 onExited == null ? void 0 : onExited.apply(void 0, args);
216 };
217
218 var Transition = transition;
219
220 if (!container || !(show || Transition && !exited)) {
221 return null;
222 }
223
224 var dialogProps = _extends({
225 role: role,
226 ref: modal.setDialogRef,
227 // apparently only works on the dialog role element
228 'aria-modal': role === 'dialog' ? true : undefined
229 }, rest, {
230 style: style,
231 className: className,
232 tabIndex: -1
233 });
234
235 var dialog = renderDialog ? renderDialog(dialogProps) : /*#__PURE__*/React.createElement("div", dialogProps, React.cloneElement(children, {
236 role: 'document'
237 }));
238
239 if (Transition) {
240 dialog = /*#__PURE__*/React.createElement(Transition, {
241 appear: true,
242 unmountOnExit: true,
243 "in": !!show,
244 onExit: onExit,
245 onExiting: onExiting,
246 onExited: handleHidden,
247 onEnter: onEnter,
248 onEntering: onEntering,
249 onEntered: onEntered
250 }, dialog);
251 }
252
253 var backdropElement = null;
254
255 if (backdrop) {
256 var BackdropTransition = backdropTransition;
257 backdropElement = renderBackdrop({
258 ref: modal.setBackdropRef,
259 onClick: handleBackdropClick
260 });
261
262 if (BackdropTransition) {
263 backdropElement = /*#__PURE__*/React.createElement(BackdropTransition, {
264 appear: true,
265 "in": !!show
266 }, backdropElement);
267 }
268 }
269
270 return /*#__PURE__*/React.createElement(React.Fragment, null, ReactDOM.createPortal( /*#__PURE__*/React.createElement(React.Fragment, null, backdropElement, dialog), container));
271});
272var propTypes = {
273 /**
274 * Set the visibility of the Modal
275 */
276 show: PropTypes.bool,
277
278 /**
279 * A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element.
280 *
281 * For the sake of assistive technologies, the container should usually be the document body, so that the rest of the
282 * page content can be placed behind a virtual backdrop as well as a visual one.
283 */
284 container: PropTypes.any,
285
286 /**
287 * A callback fired when the Modal is opening.
288 */
289 onShow: PropTypes.func,
290
291 /**
292 * A callback fired when either the backdrop is clicked, or the escape key is pressed.
293 *
294 * The `onHide` callback only signals intent from the Modal,
295 * you must actually set the `show` prop to `false` for the Modal to close.
296 */
297 onHide: PropTypes.func,
298
299 /**
300 * Include a backdrop component.
301 */
302 backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]),
303
304 /**
305 * A function that returns the dialog component. Useful for custom
306 * rendering. **Note:** the component should make sure to apply the provided ref.
307 *
308 * ```js static
309 * renderDialog={props => <MyDialog {...props} />}
310 * ```
311 */
312 renderDialog: PropTypes.func,
313
314 /**
315 * A function that returns a backdrop component. Useful for custom
316 * backdrop rendering.
317 *
318 * ```js
319 * renderBackdrop={props => <MyBackdrop {...props} />}
320 * ```
321 */
322 renderBackdrop: PropTypes.func,
323
324 /**
325 * A callback fired when the escape key, if specified in `keyboard`, is pressed.
326 */
327 onEscapeKeyDown: PropTypes.func,
328
329 /**
330 * A callback fired when the backdrop, if specified, is clicked.
331 */
332 onBackdropClick: PropTypes.func,
333
334 /**
335 * A css class or set of classes applied to the modal container when the modal is open,
336 * and removed when it is closed.
337 */
338 containerClassName: PropTypes.string,
339
340 /**
341 * Close the modal when escape key is pressed
342 */
343 keyboard: PropTypes.bool,
344
345 /**
346 * A `react-transition-group@2.0.0` `<Transition/>` component used
347 * to control animations for the dialog component.
348 */
349 transition: PropTypes.elementType,
350
351 /**
352 * A `react-transition-group@2.0.0` `<Transition/>` component used
353 * to control animations for the backdrop components.
354 */
355 backdropTransition: PropTypes.elementType,
356
357 /**
358 * When `true` The modal will automatically shift focus to itself when it opens, and
359 * replace it to the last focused element when it closes. This also
360 * works correctly with any Modal children that have the `autoFocus` prop.
361 *
362 * Generally this should never be set to `false` as it makes the Modal less
363 * accessible to assistive technologies, like screen readers.
364 */
365 autoFocus: PropTypes.bool,
366
367 /**
368 * When `true` The modal will prevent focus from leaving the Modal while open.
369 *
370 * Generally this should never be set to `false` as it makes the Modal less
371 * accessible to assistive technologies, like screen readers.
372 */
373 enforceFocus: PropTypes.bool,
374
375 /**
376 * When `true` The modal will restore focus to previously focused element once
377 * modal is hidden
378 */
379 restoreFocus: PropTypes.bool,
380
381 /**
382 * Options passed to focus function when `restoreFocus` is set to `true`
383 *
384 * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters
385 */
386 restoreFocusOptions: PropTypes.shape({
387 preventScroll: PropTypes.bool
388 }),
389
390 /**
391 * Callback fired before the Modal transitions in
392 */
393 onEnter: PropTypes.func,
394
395 /**
396 * Callback fired as the Modal begins to transition in
397 */
398 onEntering: PropTypes.func,
399
400 /**
401 * Callback fired after the Modal finishes transitioning in
402 */
403 onEntered: PropTypes.func,
404
405 /**
406 * Callback fired right before the Modal transitions out
407 */
408 onExit: PropTypes.func,
409
410 /**
411 * Callback fired as the Modal begins to transition out
412 */
413 onExiting: PropTypes.func,
414
415 /**
416 * Callback fired after the Modal finishes transitioning out
417 */
418 onExited: PropTypes.func,
419
420 /**
421 * A ModalManager instance used to track and manage the state of open
422 * Modals. Useful when customizing how modals interact within a container
423 */
424 manager: PropTypes.instanceOf(ModalManager)
425};
426Modal.displayName = 'Modal';
427Modal.propTypes = propTypes;
428export default Object.assign(Modal, {
429 Manager: ModalManager
430});
\No newline at end of file