UNPKG

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