Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

518 řádky
15 KiB

  1. import _extends from "@babel/runtime/helpers/esm/extends";
  2. import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
  3. import _assertThisInitialized from "@babel/runtime/helpers/esm/assertThisInitialized";
  4. import _inheritsLoose from "@babel/runtime/helpers/esm/inheritsLoose";
  5. /* eslint-disable react/prop-types */
  6. import activeElement from 'dom-helpers/activeElement';
  7. import contains from 'dom-helpers/contains';
  8. import canUseDOM from 'dom-helpers/canUseDOM';
  9. import listen from 'dom-helpers/listen';
  10. import PropTypes from 'prop-types';
  11. import React from 'react';
  12. import ReactDOM from 'react-dom';
  13. import ModalManager from './ModalManager';
  14. import ownerDocument from './utils/ownerDocument';
  15. import useWaitForDOMRef from './utils/useWaitForDOMRef';
  16. function omitProps(props, propTypes) {
  17. var keys = Object.keys(props);
  18. var newProps = {};
  19. keys.forEach(function (prop) {
  20. if (!Object.prototype.hasOwnProperty.call(propTypes, prop)) {
  21. newProps[prop] = props[prop];
  22. }
  23. });
  24. return newProps;
  25. }
  26. var manager;
  27. /**
  28. * Love them or hate them, `<Modal />` provides a solid foundation for creating dialogs, lightboxes, or whatever else.
  29. * The Modal component renders its `children` node in front of a backdrop component.
  30. *
  31. * The Modal offers a few helpful features over using just a `<Portal/>` component and some styles:
  32. *
  33. * - Manages dialog stacking when one-at-a-time just isn't enough.
  34. * - Creates a backdrop, for disabling interaction below the modal.
  35. * - It properly manages focus; moving to the modal content, and keeping it there until the modal is closed.
  36. * - It disables scrolling of the page content while open.
  37. * - Adds the appropriate ARIA roles are automatically.
  38. * - Easily pluggable animations via a `<Transition/>` component.
  39. *
  40. * Note that, in the same way the backdrop element prevents users from clicking or interacting
  41. * with the page content underneath the Modal, Screen readers also need to be signaled to not to
  42. * interact with page content while the Modal is open. To do this, we use a common technique of applying
  43. * the `aria-hidden='true'` attribute to the non-Modal elements in the Modal `container`. This means that for
  44. * a Modal to be truly modal, it should have a `container` that is _outside_ your app's
  45. * React hierarchy (such as the default: document.body).
  46. */
  47. var Modal =
  48. /*#__PURE__*/
  49. function (_React$Component) {
  50. _inheritsLoose(Modal, _React$Component);
  51. function Modal() {
  52. var _this;
  53. for (var _len = arguments.length, _args = new Array(_len), _key = 0; _key < _len; _key++) {
  54. _args[_key] = arguments[_key];
  55. }
  56. _this = _React$Component.call.apply(_React$Component, [this].concat(_args)) || this;
  57. _this.state = {
  58. exited: !_this.props.show
  59. };
  60. _this.onShow = function () {
  61. var _this$props = _this.props,
  62. container = _this$props.container,
  63. containerClassName = _this$props.containerClassName,
  64. onShow = _this$props.onShow;
  65. _this.getModalManager().add(_assertThisInitialized(_this), container, containerClassName);
  66. _this.removeKeydownListener = listen(document, 'keydown', _this.handleDocumentKeyDown);
  67. _this.removeFocusListener = listen(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted
  68. // and so steals focus from it
  69. function () {
  70. return setTimeout(_this.enforceFocus);
  71. }, true);
  72. if (onShow) {
  73. onShow();
  74. } // autofocus after onShow, to not trigger a focus event for previous
  75. // modals before this one is shown.
  76. _this.autoFocus();
  77. };
  78. _this.onHide = function () {
  79. _this.getModalManager().remove(_assertThisInitialized(_this));
  80. _this.removeKeydownListener();
  81. _this.removeFocusListener();
  82. if (_this.props.restoreFocus) {
  83. _this.restoreLastFocus();
  84. }
  85. };
  86. _this.setDialogRef = function (ref) {
  87. _this.dialog = ref;
  88. };
  89. _this.setBackdropRef = function (ref) {
  90. _this.backdrop = ref && ReactDOM.findDOMNode(ref);
  91. };
  92. _this.handleHidden = function () {
  93. _this.setState({
  94. exited: true
  95. });
  96. _this.onHide();
  97. if (_this.props.onExited) {
  98. var _this$props2;
  99. (_this$props2 = _this.props).onExited.apply(_this$props2, arguments);
  100. }
  101. };
  102. _this.handleBackdropClick = function (e) {
  103. if (e.target !== e.currentTarget) {
  104. return;
  105. }
  106. if (_this.props.onBackdropClick) {
  107. _this.props.onBackdropClick(e);
  108. }
  109. if (_this.props.backdrop === true) {
  110. _this.props.onHide();
  111. }
  112. };
  113. _this.handleDocumentKeyDown = function (e) {
  114. if (_this.props.keyboard && e.keyCode === 27 && _this.isTopModal()) {
  115. if (_this.props.onEscapeKeyDown) {
  116. _this.props.onEscapeKeyDown(e);
  117. }
  118. _this.props.onHide();
  119. }
  120. };
  121. _this.enforceFocus = function () {
  122. if (!_this.props.enforceFocus || !_this._isMounted || !_this.isTopModal()) {
  123. return;
  124. }
  125. var currentActiveElement = activeElement(ownerDocument(_assertThisInitialized(_this)));
  126. if (_this.dialog && !contains(_this.dialog, currentActiveElement)) {
  127. _this.dialog.focus();
  128. }
  129. };
  130. _this.renderBackdrop = function () {
  131. var _this$props3 = _this.props,
  132. renderBackdrop = _this$props3.renderBackdrop,
  133. Transition = _this$props3.backdropTransition;
  134. var backdrop = renderBackdrop({
  135. ref: _this.setBackdropRef,
  136. onClick: _this.handleBackdropClick
  137. });
  138. if (Transition) {
  139. backdrop = React.createElement(Transition, {
  140. appear: true,
  141. "in": _this.props.show
  142. }, backdrop);
  143. }
  144. return backdrop;
  145. };
  146. return _this;
  147. }
  148. Modal.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps) {
  149. if (nextProps.show) {
  150. return {
  151. exited: false
  152. };
  153. }
  154. if (!nextProps.transition) {
  155. // Otherwise let handleHidden take care of marking exited.
  156. return {
  157. exited: true
  158. };
  159. }
  160. return null;
  161. };
  162. var _proto = Modal.prototype;
  163. _proto.componentDidMount = function componentDidMount() {
  164. this._isMounted = true;
  165. if (this.props.show) {
  166. this.onShow();
  167. }
  168. };
  169. _proto.componentDidUpdate = function componentDidUpdate(prevProps) {
  170. var transition = this.props.transition;
  171. if (prevProps.show && !this.props.show && !transition) {
  172. // Otherwise handleHidden will call this.
  173. this.onHide();
  174. } else if (!prevProps.show && this.props.show) {
  175. this.onShow();
  176. }
  177. };
  178. _proto.componentWillUnmount = function componentWillUnmount() {
  179. var _this$props4 = this.props,
  180. show = _this$props4.show,
  181. transition = _this$props4.transition;
  182. this._isMounted = false;
  183. if (show || transition && !this.state.exited) {
  184. this.onHide();
  185. }
  186. };
  187. _proto.getSnapshotBeforeUpdate = function getSnapshotBeforeUpdate(prevProps) {
  188. if (canUseDOM && !prevProps.show && this.props.show) {
  189. this.lastFocus = activeElement();
  190. }
  191. return null;
  192. };
  193. _proto.getModalManager = function getModalManager() {
  194. if (this.props.manager) {
  195. return this.props.manager;
  196. }
  197. if (!manager) {
  198. manager = new ModalManager();
  199. }
  200. return manager;
  201. };
  202. _proto.restoreLastFocus = function restoreLastFocus() {
  203. // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917)
  204. if (this.lastFocus && this.lastFocus.focus) {
  205. this.lastFocus.focus(this.props.restoreFocusOptions);
  206. this.lastFocus = null;
  207. }
  208. };
  209. _proto.autoFocus = function autoFocus() {
  210. if (!this.props.autoFocus) return;
  211. var currentActiveElement = activeElement(ownerDocument(this));
  212. if (this.dialog && !contains(this.dialog, currentActiveElement)) {
  213. this.lastFocus = currentActiveElement;
  214. this.dialog.focus();
  215. }
  216. };
  217. _proto.isTopModal = function isTopModal() {
  218. return this.getModalManager().isTopModal(this);
  219. };
  220. _proto.render = function render() {
  221. var _this$props5 = this.props,
  222. show = _this$props5.show,
  223. container = _this$props5.container,
  224. children = _this$props5.children,
  225. renderDialog = _this$props5.renderDialog,
  226. _this$props5$role = _this$props5.role,
  227. role = _this$props5$role === void 0 ? 'dialog' : _this$props5$role,
  228. Transition = _this$props5.transition,
  229. backdrop = _this$props5.backdrop,
  230. className = _this$props5.className,
  231. style = _this$props5.style,
  232. onExit = _this$props5.onExit,
  233. onExiting = _this$props5.onExiting,
  234. onEnter = _this$props5.onEnter,
  235. onEntering = _this$props5.onEntering,
  236. onEntered = _this$props5.onEntered,
  237. props = _objectWithoutPropertiesLoose(_this$props5, ["show", "container", "children", "renderDialog", "role", "transition", "backdrop", "className", "style", "onExit", "onExiting", "onEnter", "onEntering", "onEntered"]);
  238. if (!(show || Transition && !this.state.exited)) {
  239. return null;
  240. }
  241. var dialogProps = _extends({
  242. role: role,
  243. ref: this.setDialogRef,
  244. // apparently only works on the dialog role element
  245. 'aria-modal': role === 'dialog' ? true : undefined
  246. }, omitProps(props, Modal.propTypes), {
  247. style: style,
  248. className: className,
  249. tabIndex: '-1'
  250. });
  251. var dialog = renderDialog ? renderDialog(dialogProps) : React.createElement("div", dialogProps, React.cloneElement(children, {
  252. role: 'document'
  253. }));
  254. if (Transition) {
  255. dialog = React.createElement(Transition, {
  256. appear: true,
  257. unmountOnExit: true,
  258. "in": show,
  259. onExit: onExit,
  260. onExiting: onExiting,
  261. onExited: this.handleHidden,
  262. onEnter: onEnter,
  263. onEntering: onEntering,
  264. onEntered: onEntered
  265. }, dialog);
  266. }
  267. return ReactDOM.createPortal(React.createElement(React.Fragment, null, backdrop && this.renderBackdrop(), dialog), container);
  268. };
  269. return Modal;
  270. }(React.Component); // dumb HOC for the sake react-docgen
  271. Modal.propTypes = {
  272. /**
  273. * Set the visibility of the Modal
  274. */
  275. show: PropTypes.bool,
  276. /**
  277. * A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element.
  278. *
  279. * For the sake of assistive technologies, the container should usually be the document body, so that the rest of the
  280. * page content can be placed behind a virtual backdrop as well as a visual one.
  281. */
  282. container: PropTypes.any,
  283. /**
  284. * A callback fired when the Modal is opening.
  285. */
  286. onShow: PropTypes.func,
  287. /**
  288. * A callback fired when either the backdrop is clicked, or the escape key is pressed.
  289. *
  290. * The `onHide` callback only signals intent from the Modal,
  291. * you must actually set the `show` prop to `false` for the Modal to close.
  292. */
  293. onHide: PropTypes.func,
  294. /**
  295. * Include a backdrop component.
  296. */
  297. backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]),
  298. /**
  299. * A function that returns the dialog component. Useful for custom
  300. * rendering. **Note:** the component should make sure to apply the provided ref.
  301. *
  302. * ```js
  303. * renderDialog={props => <MyDialog {...props} />}
  304. * ```
  305. */
  306. renderDialog: PropTypes.func,
  307. /**
  308. * A function that returns a backdrop component. Useful for custom
  309. * backdrop rendering.
  310. *
  311. * ```js
  312. * renderBackdrop={props => <MyBackdrop {...props} />}
  313. * ```
  314. */
  315. renderBackdrop: PropTypes.func,
  316. /**
  317. * A callback fired when the escape key, if specified in `keyboard`, is pressed.
  318. */
  319. onEscapeKeyDown: PropTypes.func,
  320. /**
  321. * A callback fired when the backdrop, if specified, is clicked.
  322. */
  323. onBackdropClick: PropTypes.func,
  324. /**
  325. * A css class or set of classes applied to the modal container when the modal is open,
  326. * and removed when it is closed.
  327. */
  328. containerClassName: PropTypes.string,
  329. /**
  330. * Close the modal when escape key is pressed
  331. */
  332. keyboard: PropTypes.bool,
  333. /**
  334. * A `react-transition-group@2.0.0` `<Transition/>` component used
  335. * to control animations for the dialog component.
  336. */
  337. transition: PropTypes.elementType,
  338. /**
  339. * A `react-transition-group@2.0.0` `<Transition/>` component used
  340. * to control animations for the backdrop components.
  341. */
  342. backdropTransition: PropTypes.elementType,
  343. /**
  344. * When `true` The modal will automatically shift focus to itself when it opens, and
  345. * replace it to the last focused element when it closes. This also
  346. * works correctly with any Modal children that have the `autoFocus` prop.
  347. *
  348. * Generally this should never be set to `false` as it makes the Modal less
  349. * accessible to assistive technologies, like screen readers.
  350. */
  351. autoFocus: PropTypes.bool,
  352. /**
  353. * When `true` The modal will prevent focus from leaving the Modal while open.
  354. *
  355. * Generally this should never be set to `false` as it makes the Modal less
  356. * accessible to assistive technologies, like screen readers.
  357. */
  358. enforceFocus: PropTypes.bool,
  359. /**
  360. * When `true` The modal will restore focus to previously focused element once
  361. * modal is hidden
  362. */
  363. restoreFocus: PropTypes.bool,
  364. /**
  365. * Options passed to focus function when `restoreFocus` is set to `true`
  366. *
  367. * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters
  368. */
  369. restoreFocusOptions: PropTypes.shape({
  370. preventScroll: PropTypes.bool
  371. }),
  372. /**
  373. * Callback fired before the Modal transitions in
  374. */
  375. onEnter: PropTypes.func,
  376. /**
  377. * Callback fired as the Modal begins to transition in
  378. */
  379. onEntering: PropTypes.func,
  380. /**
  381. * Callback fired after the Modal finishes transitioning in
  382. */
  383. onEntered: PropTypes.func,
  384. /**
  385. * Callback fired right before the Modal transitions out
  386. */
  387. onExit: PropTypes.func,
  388. /**
  389. * Callback fired as the Modal begins to transition out
  390. */
  391. onExiting: PropTypes.func,
  392. /**
  393. * Callback fired after the Modal finishes transitioning out
  394. */
  395. onExited: PropTypes.func,
  396. /**
  397. * A ModalManager instance used to track and manage the state of open
  398. * Modals. Useful when customizing how modals interact within a container
  399. */
  400. manager: PropTypes.object
  401. };
  402. Modal.defaultProps = {
  403. show: false,
  404. role: 'dialog',
  405. backdrop: true,
  406. keyboard: true,
  407. autoFocus: true,
  408. enforceFocus: true,
  409. restoreFocus: true,
  410. onHide: function onHide() {},
  411. renderBackdrop: function renderBackdrop(props) {
  412. return React.createElement("div", props);
  413. }
  414. };
  415. function forwardRef(Component) {
  416. // eslint-disable-next-line react/display-name
  417. var ModalWithContainer = React.forwardRef(function (props, ref) {
  418. var resolved = useWaitForDOMRef(props.container);
  419. return resolved ? React.createElement(Component, _extends({}, props, {
  420. ref: ref,
  421. container: resolved
  422. })) : null;
  423. });
  424. ModalWithContainer.Manager = ModalManager;
  425. ModalWithContainer._Inner = Component;
  426. return ModalWithContainer;
  427. }
  428. var ModalWithContainer = forwardRef(Modal);
  429. ModalWithContainer.Manager = ModalManager;
  430. export default ModalWithContainer;