UNPKG

22.7 kBJavaScriptView Raw
1function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
2function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
3function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
4function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
5function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
6function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
7function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
8function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
9function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
10function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
11function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
12function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
13function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
14function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
15function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
16import React from 'react';
17import PropTypes from 'prop-types';
18import classNames from 'classnames';
19import Portal from './Portal';
20import Fade from './Fade';
21import { getOriginalBodyPadding, conditionallyUpdateScrollbar, setScrollbarWidth, mapToCssModules, omit, focusableElements, TransitionTimeouts, keyCodes, targetPropType, getTarget } from './utils';
22function noop() {}
23var FadePropTypes = PropTypes.shape(Fade.propTypes);
24var propTypes = {
25 /** */
26 autoFocus: PropTypes.bool,
27 /** Add backdrop to modal */
28 backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]),
29 /** add custom classname to backdrop */
30 backdropClassName: PropTypes.string,
31 backdropTransition: FadePropTypes,
32 /** Vertically center the modal */
33 centered: PropTypes.bool,
34 /** Add children for the modal to wrap */
35 children: PropTypes.node,
36 /** Add custom className for modal content */
37 contentClassName: PropTypes.string,
38 className: PropTypes.string,
39 container: targetPropType,
40 cssModule: PropTypes.object,
41 external: PropTypes.node,
42 /** Enable/Disable animation */
43 fade: PropTypes.bool,
44 /** Make the modal fullscreen */
45 fullscreen: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['sm', 'md', 'lg', 'xl'])]),
46 innerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.func]),
47 /** The status of the modal, either open or close */
48 isOpen: PropTypes.bool,
49 /** Allow modal to be closed with escape key. */
50 keyboard: PropTypes.bool,
51 /** Identifies the element (or elements) that labels the current element. */
52 labelledBy: PropTypes.string,
53 modalClassName: PropTypes.string,
54 modalTransition: FadePropTypes,
55 /** Function to be triggered on close */
56 onClosed: PropTypes.func,
57 /** Function to be triggered on enter */
58 onEnter: PropTypes.func,
59 /** Function to be triggered on exit */
60 onExit: PropTypes.func,
61 /** Function to be triggered on open */
62 onOpened: PropTypes.func,
63 /** Returns focus to the element that triggered opening of the modal */
64 returnFocusAfterClose: PropTypes.bool,
65 /** Accessibility role */
66 role: PropTypes.string,
67 /** Make the modal scrollable */
68 scrollable: PropTypes.bool,
69 /** Two optional sizes `lg` and `sm` */
70 size: PropTypes.string,
71 /** Function to toggle modal visibility */
72 toggle: PropTypes.func,
73 trapFocus: PropTypes.bool,
74 /** Unmounts the modal when modal is closed */
75 unmountOnClose: PropTypes.bool,
76 wrapClassName: PropTypes.string,
77 zIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
78};
79var propsToOmit = Object.keys(propTypes);
80var defaultProps = {
81 isOpen: false,
82 autoFocus: true,
83 centered: false,
84 scrollable: false,
85 role: 'dialog',
86 backdrop: true,
87 keyboard: true,
88 zIndex: 1050,
89 fade: true,
90 onOpened: noop,
91 onClosed: noop,
92 modalTransition: {
93 timeout: TransitionTimeouts.Modal
94 },
95 backdropTransition: {
96 mountOnEnter: true,
97 timeout: TransitionTimeouts.Fade // uses standard fade transition
98 },
99
100 unmountOnClose: true,
101 returnFocusAfterClose: true,
102 container: 'body',
103 trapFocus: false
104};
105var Modal = /*#__PURE__*/function (_React$Component) {
106 _inherits(Modal, _React$Component);
107 var _super = _createSuper(Modal);
108 function Modal(props) {
109 var _this;
110 _classCallCheck(this, Modal);
111 _this = _super.call(this, props);
112 _this._element = null;
113 _this._originalBodyPadding = null;
114 _this.getFocusableChildren = _this.getFocusableChildren.bind(_assertThisInitialized(_this));
115 _this.handleBackdropClick = _this.handleBackdropClick.bind(_assertThisInitialized(_this));
116 _this.handleBackdropMouseDown = _this.handleBackdropMouseDown.bind(_assertThisInitialized(_this));
117 _this.handleEscape = _this.handleEscape.bind(_assertThisInitialized(_this));
118 _this.handleStaticBackdropAnimation = _this.handleStaticBackdropAnimation.bind(_assertThisInitialized(_this));
119 _this.handleTab = _this.handleTab.bind(_assertThisInitialized(_this));
120 _this.onOpened = _this.onOpened.bind(_assertThisInitialized(_this));
121 _this.onClosed = _this.onClosed.bind(_assertThisInitialized(_this));
122 _this.manageFocusAfterClose = _this.manageFocusAfterClose.bind(_assertThisInitialized(_this));
123 _this.clearBackdropAnimationTimeout = _this.clearBackdropAnimationTimeout.bind(_assertThisInitialized(_this));
124 _this.trapFocus = _this.trapFocus.bind(_assertThisInitialized(_this));
125 _this.state = {
126 isOpen: false,
127 showStaticBackdropAnimation: false
128 };
129 return _this;
130 }
131 _createClass(Modal, [{
132 key: "componentDidMount",
133 value: function componentDidMount() {
134 var _this$props = this.props,
135 isOpen = _this$props.isOpen,
136 autoFocus = _this$props.autoFocus,
137 onEnter = _this$props.onEnter;
138 if (isOpen) {
139 this.init();
140 this.setState({
141 isOpen: true
142 });
143 if (autoFocus) {
144 this.setFocus();
145 }
146 }
147 if (onEnter) {
148 onEnter();
149 }
150
151 // traps focus inside the Modal, even if the browser address bar is focused
152 document.addEventListener('focus', this.trapFocus, true);
153 this._isMounted = true;
154 }
155 }, {
156 key: "componentDidUpdate",
157 value: function componentDidUpdate(prevProps, prevState) {
158 if (this.props.isOpen && !prevProps.isOpen) {
159 this.init();
160 this.setState({
161 isOpen: true
162 });
163 // let render() renders Modal Dialog first
164 return;
165 }
166
167 // now Modal Dialog is rendered and we can refer this._element and this._dialog
168 if (this.props.autoFocus && this.state.isOpen && !prevState.isOpen) {
169 this.setFocus();
170 }
171 if (this._element && prevProps.zIndex !== this.props.zIndex) {
172 this._element.style.zIndex = this.props.zIndex;
173 }
174 }
175 }, {
176 key: "componentWillUnmount",
177 value: function componentWillUnmount() {
178 this.clearBackdropAnimationTimeout();
179 if (this.props.onExit) {
180 this.props.onExit();
181 }
182 if (this._element) {
183 this.destroy();
184 if (this.props.isOpen || this.state.isOpen) {
185 this.close();
186 }
187 }
188 document.removeEventListener('focus', this.trapFocus, true);
189 this._isMounted = false;
190 }
191
192 // not mouseUp because scrollbar fires it, shouldn't close when user scrolls
193 }, {
194 key: "handleBackdropClick",
195 value: function handleBackdropClick(e) {
196 if (e.target === this._mouseDownElement) {
197 e.stopPropagation();
198 var backdrop = this._dialog ? this._dialog.parentNode : null;
199 if (backdrop && e.target === backdrop && this.props.backdrop === 'static') {
200 this.handleStaticBackdropAnimation();
201 }
202 if (!this.props.isOpen || this.props.backdrop !== true) return;
203 if (backdrop && e.target === backdrop && this.props.toggle) {
204 this.props.toggle(e);
205 }
206 }
207 }
208 }, {
209 key: "handleTab",
210 value: function handleTab(e) {
211 if (e.which !== 9) return;
212 if (this.modalIndex < Modal.openCount - 1) return; // last opened modal
213
214 var focusableChildren = this.getFocusableChildren();
215 var totalFocusable = focusableChildren.length;
216 if (totalFocusable === 0) return;
217 var currentFocus = this.getFocusedChild();
218 var focusedIndex = 0;
219 for (var i = 0; i < totalFocusable; i += 1) {
220 if (focusableChildren[i] === currentFocus) {
221 focusedIndex = i;
222 break;
223 }
224 }
225 if (e.shiftKey && focusedIndex === 0) {
226 e.preventDefault();
227 focusableChildren[totalFocusable - 1].focus();
228 } else if (!e.shiftKey && focusedIndex === totalFocusable - 1) {
229 e.preventDefault();
230 focusableChildren[0].focus();
231 }
232 }
233 }, {
234 key: "handleBackdropMouseDown",
235 value: function handleBackdropMouseDown(e) {
236 this._mouseDownElement = e.target;
237 }
238 }, {
239 key: "handleEscape",
240 value: function handleEscape(e) {
241 if (this.props.isOpen && e.keyCode === keyCodes.esc && this.props.toggle) {
242 if (this.props.keyboard) {
243 e.preventDefault();
244 e.stopPropagation();
245 this.props.toggle(e);
246 } else if (this.props.backdrop === 'static') {
247 e.preventDefault();
248 e.stopPropagation();
249 this.handleStaticBackdropAnimation();
250 }
251 }
252 }
253 }, {
254 key: "handleStaticBackdropAnimation",
255 value: function handleStaticBackdropAnimation() {
256 var _this2 = this;
257 this.clearBackdropAnimationTimeout();
258 this.setState({
259 showStaticBackdropAnimation: true
260 });
261 this._backdropAnimationTimeout = setTimeout(function () {
262 _this2.setState({
263 showStaticBackdropAnimation: false
264 });
265 }, 100);
266 }
267 }, {
268 key: "onOpened",
269 value: function onOpened(node, isAppearing) {
270 this.props.onOpened();
271 (this.props.modalTransition.onEntered || noop)(node, isAppearing);
272 }
273 }, {
274 key: "onClosed",
275 value: function onClosed(node) {
276 var unmountOnClose = this.props.unmountOnClose;
277 // so all methods get called before it is unmounted
278 this.props.onClosed();
279 (this.props.modalTransition.onExited || noop)(node);
280 if (unmountOnClose) {
281 this.destroy();
282 }
283 this.close();
284 if (this._isMounted) {
285 this.setState({
286 isOpen: false
287 });
288 }
289 }
290 }, {
291 key: "setFocus",
292 value: function setFocus() {
293 if (this._dialog && this._dialog.parentNode && typeof this._dialog.parentNode.focus === 'function') {
294 this._dialog.parentNode.focus();
295 }
296 }
297 }, {
298 key: "getFocusableChildren",
299 value: function getFocusableChildren() {
300 return this._element.querySelectorAll(focusableElements.join(', '));
301 }
302 }, {
303 key: "getFocusedChild",
304 value: function getFocusedChild() {
305 var currentFocus;
306 var focusableChildren = this.getFocusableChildren();
307 try {
308 currentFocus = document.activeElement;
309 } catch (err) {
310 currentFocus = focusableChildren[0];
311 }
312 return currentFocus;
313 }
314 }, {
315 key: "trapFocus",
316 value: function trapFocus(ev) {
317 if (!this.props.trapFocus) {
318 return;
319 }
320 if (!this._element) {
321 // element is not attached
322 return;
323 }
324 if (this._dialog && this._dialog.parentNode === ev.target) {
325 // initial focus when the Modal is opened
326 return;
327 }
328 if (this.modalIndex < Modal.openCount - 1) {
329 // last opened modal
330 return;
331 }
332 var children = this.getFocusableChildren();
333 for (var i = 0; i < children.length; i += 1) {
334 // focus is already inside the Modal
335 if (children[i] === ev.target) return;
336 }
337 if (children.length > 0) {
338 // otherwise focus the first focusable element in the Modal
339 ev.preventDefault();
340 ev.stopPropagation();
341 children[0].focus();
342 }
343 }
344 }, {
345 key: "init",
346 value: function init() {
347 try {
348 this._triggeringElement = document.activeElement;
349 } catch (err) {
350 this._triggeringElement = null;
351 }
352 if (!this._element) {
353 this._element = document.createElement('div');
354 this._element.setAttribute('tabindex', '-1');
355 this._element.style.position = 'relative';
356 this._element.style.zIndex = this.props.zIndex;
357 this._mountContainer = getTarget(this.props.container);
358 this._mountContainer.appendChild(this._element);
359 }
360 this._originalBodyPadding = getOriginalBodyPadding();
361 if (Modal.openCount < 1) {
362 Modal.originalBodyOverflow = window.getComputedStyle(document.body).overflow;
363 }
364 conditionallyUpdateScrollbar();
365 if (Modal.openCount === 0) {
366 document.body.className = classNames(document.body.className, mapToCssModules('modal-open', this.props.cssModule));
367 document.body.style.overflow = 'hidden';
368 }
369 this.modalIndex = Modal.openCount;
370 Modal.openCount += 1;
371 }
372 }, {
373 key: "destroy",
374 value: function destroy() {
375 if (this._element) {
376 this._mountContainer.removeChild(this._element);
377 this._element = null;
378 }
379 this.manageFocusAfterClose();
380 }
381 }, {
382 key: "manageFocusAfterClose",
383 value: function manageFocusAfterClose() {
384 if (this._triggeringElement) {
385 var returnFocusAfterClose = this.props.returnFocusAfterClose;
386 if (this._triggeringElement.focus && returnFocusAfterClose) this._triggeringElement.focus();
387 this._triggeringElement = null;
388 }
389 }
390 }, {
391 key: "close",
392 value: function close() {
393 if (Modal.openCount <= 1) {
394 var modalOpenClassName = mapToCssModules('modal-open', this.props.cssModule);
395 // Use regex to prevent matching `modal-open` as part of a different class, e.g. `my-modal-opened`
396 var modalOpenClassNameRegex = new RegExp("(^| )".concat(modalOpenClassName, "( |$)"));
397 document.body.className = document.body.className.replace(modalOpenClassNameRegex, ' ').trim();
398 document.body.style.overflow = Modal.originalBodyOverflow;
399 }
400 this.manageFocusAfterClose();
401 Modal.openCount = Math.max(0, Modal.openCount - 1);
402 setScrollbarWidth(this._originalBodyPadding);
403 }
404 }, {
405 key: "clearBackdropAnimationTimeout",
406 value: function clearBackdropAnimationTimeout() {
407 if (this._backdropAnimationTimeout) {
408 clearTimeout(this._backdropAnimationTimeout);
409 this._backdropAnimationTimeout = undefined;
410 }
411 }
412 }, {
413 key: "renderModalDialog",
414 value: function renderModalDialog() {
415 var _classNames,
416 _this3 = this;
417 var attributes = omit(this.props, propsToOmit);
418 var dialogBaseClass = 'modal-dialog';
419 return /*#__PURE__*/React.createElement("div", _extends({}, attributes, {
420 className: mapToCssModules(classNames(dialogBaseClass, this.props.className, (_classNames = {}, _defineProperty(_classNames, "modal-".concat(this.props.size), this.props.size), _defineProperty(_classNames, "".concat(dialogBaseClass, "-centered"), this.props.centered), _defineProperty(_classNames, "".concat(dialogBaseClass, "-scrollable"), this.props.scrollable), _defineProperty(_classNames, 'modal-fullscreen', this.props.fullscreen === true), _defineProperty(_classNames, "modal-fullscreen-".concat(this.props.fullscreen, "-down"), typeof this.props.fullscreen === 'string'), _classNames)), this.props.cssModule),
421 role: "document",
422 ref: function ref(c) {
423 _this3._dialog = c;
424 }
425 }), /*#__PURE__*/React.createElement("div", {
426 className: mapToCssModules(classNames('modal-content', this.props.contentClassName), this.props.cssModule)
427 }, this.props.children));
428 }
429 }, {
430 key: "render",
431 value: function render() {
432 var unmountOnClose = this.props.unmountOnClose;
433 if (!!this._element && (this.state.isOpen || !unmountOnClose)) {
434 var isModalHidden = !!this._element && !this.state.isOpen && !unmountOnClose;
435 this._element.style.display = isModalHidden ? 'none' : 'block';
436 var _this$props2 = this.props,
437 wrapClassName = _this$props2.wrapClassName,
438 modalClassName = _this$props2.modalClassName,
439 backdropClassName = _this$props2.backdropClassName,
440 cssModule = _this$props2.cssModule,
441 isOpen = _this$props2.isOpen,
442 backdrop = _this$props2.backdrop,
443 role = _this$props2.role,
444 labelledBy = _this$props2.labelledBy,
445 external = _this$props2.external,
446 innerRef = _this$props2.innerRef;
447 var modalAttributes = {
448 onClick: this.handleBackdropClick,
449 onMouseDown: this.handleBackdropMouseDown,
450 onKeyUp: this.handleEscape,
451 onKeyDown: this.handleTab,
452 style: {
453 display: 'block'
454 },
455 'aria-labelledby': labelledBy,
456 'aria-modal': true,
457 role: role,
458 tabIndex: '-1'
459 };
460 var hasTransition = this.props.fade;
461 var modalTransition = _objectSpread(_objectSpread(_objectSpread({}, Fade.defaultProps), this.props.modalTransition), {}, {
462 baseClass: hasTransition ? this.props.modalTransition.baseClass : '',
463 timeout: hasTransition ? this.props.modalTransition.timeout : 0
464 });
465 var backdropTransition = _objectSpread(_objectSpread(_objectSpread({}, Fade.defaultProps), this.props.backdropTransition), {}, {
466 baseClass: hasTransition ? this.props.backdropTransition.baseClass : '',
467 timeout: hasTransition ? this.props.backdropTransition.timeout : 0
468 });
469 var Backdrop = backdrop && (hasTransition ? /*#__PURE__*/React.createElement(Fade, _extends({}, backdropTransition, {
470 "in": isOpen && !!backdrop,
471 cssModule: cssModule,
472 className: mapToCssModules(classNames('modal-backdrop', backdropClassName), cssModule)
473 })) : /*#__PURE__*/React.createElement("div", {
474 className: mapToCssModules(classNames('modal-backdrop', 'show', backdropClassName), cssModule)
475 }));
476 return /*#__PURE__*/React.createElement(Portal, {
477 node: this._element
478 }, /*#__PURE__*/React.createElement("div", {
479 className: mapToCssModules(wrapClassName)
480 }, /*#__PURE__*/React.createElement(Fade, _extends({}, modalAttributes, modalTransition, {
481 "in": isOpen,
482 onEntered: this.onOpened,
483 onExited: this.onClosed,
484 cssModule: cssModule,
485 className: mapToCssModules(classNames('modal', modalClassName, this.state.showStaticBackdropAnimation && 'modal-static'), cssModule),
486 innerRef: innerRef
487 }), external, this.renderModalDialog()), Backdrop));
488 }
489 return null;
490 }
491 }]);
492 return Modal;
493}(React.Component);
494Modal.propTypes = propTypes;
495Modal.defaultProps = defaultProps;
496Modal.openCount = 0;
497Modal.originalBodyOverflow = null;
498export default Modal;
\No newline at end of file