UNPKG

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