您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

StatelessDialog.web.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // @flow
  2. import Button, { ButtonGroup } from '@atlaskit/button';
  3. import { withContextFromProps } from '@atlaskit/layer-manager';
  4. import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
  5. import _ from 'lodash';
  6. import PropTypes from 'prop-types';
  7. import React, { Component } from 'react';
  8. import { translate } from '../../i18n';
  9. import type { DialogProps } from '../constants';
  10. /**
  11. * The ID to be used for the cancel button if enabled.
  12. * @type {string}
  13. */
  14. const CANCEL_BUTTON_ID = 'modal-dialog-cancel-button';
  15. /**
  16. * The ID to be used for the ok button if enabled.
  17. * @type {string}
  18. */
  19. const OK_BUTTON_ID = 'modal-dialog-ok-button';
  20. /**
  21. * The type of the React {@code Component} props of {@link StatelessDialog}.
  22. *
  23. * @static
  24. */
  25. type Props = {
  26. ...DialogProps,
  27. i18n: Object,
  28. /**
  29. * Disables dismissing the dialog when the blanket is clicked. Enabled
  30. * by default.
  31. */
  32. disableBlanketClickDismiss: boolean,
  33. /**
  34. * Whether the dialog is modal. This means clicking on the blanket will
  35. * leave the dialog open. No cancel button.
  36. */
  37. isModal: boolean,
  38. /**
  39. * Disables rendering of the submit button.
  40. */
  41. submitDisabled: boolean,
  42. /**
  43. * Width of the dialog, can be:
  44. * - 'small' (400px), 'medium' (600px), 'large' (800px),
  45. * 'x-large' (968px)
  46. * - integer value for pixel width
  47. * - string value for percentage
  48. */
  49. width: string
  50. };
  51. /**
  52. * ContexTypes is used as a workaround for Atlaskit's modal being displayed
  53. * outside of the normal App hierarchy, thereby losing context. ContextType
  54. * is responsible for taking its props and passing them into children.
  55. *
  56. * @type {ReactElement}
  57. */
  58. const ContextProvider = withContextFromProps({
  59. i18n: PropTypes.object
  60. });
  61. /**
  62. * Web dialog that uses atlaskit modal-dialog to display dialogs.
  63. */
  64. class StatelessDialog extends Component<Props> {
  65. /**
  66. * The functional component to be used for rendering the modal footer.
  67. */
  68. _Footer: ?Function
  69. _dialogElement: ?HTMLElement;
  70. /**
  71. * Initializes a new {@code StatelessDialog} instance.
  72. *
  73. * @param {Object} props - The read-only properties with which the new
  74. * instance is to be initialized.
  75. */
  76. constructor(props) {
  77. super(props);
  78. // Bind event handlers so they are only bound once for every instance.
  79. this._onCancel = this._onCancel.bind(this);
  80. this._onDialogDismissed = this._onDialogDismissed.bind(this);
  81. this._onKeyDown = this._onKeyDown.bind(this);
  82. this._onSubmit = this._onSubmit.bind(this);
  83. this._setDialogElement = this._setDialogElement.bind(this);
  84. this._Footer = this._createFooterConstructor(props);
  85. }
  86. /**
  87. * React Component method that executes before the component is updated.
  88. *
  89. * @inheritdoc
  90. * @param {Object} nextProps - The next properties, before the update.
  91. * @returns {void}
  92. */
  93. componentWillUpdate(nextProps) {
  94. // If button states have changed, update the Footer constructor function
  95. // so buttons of the proper state are rendered.
  96. if (nextProps.okDisabled !== this.props.okDisabled
  97. || nextProps.cancelDisabled !== this.props.cancelDisabled
  98. || nextProps.submitDisabled !== this.props.submitDisabled) {
  99. this._Footer = this._createFooterConstructor(nextProps);
  100. }
  101. }
  102. /**
  103. * Implements React's {@link Component#render()}.
  104. *
  105. * @inheritdoc
  106. * @returns {ReactElement}
  107. */
  108. render() {
  109. const {
  110. children,
  111. t /* The following fixes a flow error: */ = _.identity,
  112. titleString,
  113. titleKey,
  114. width
  115. } = this.props;
  116. return (
  117. <Modal
  118. autoFocus = { true }
  119. footer = { this._Footer }
  120. heading = { titleString || t(titleKey) }
  121. i18n = { this.props.i18n }
  122. onClose = { this._onDialogDismissed }
  123. onDialogDismissed = { this._onDialogDismissed }
  124. shouldCloseOnEscapePress = { true }
  125. width = { width || 'medium' }>
  126. {
  127. /**
  128. * Wrapping the contents of {@link Modal} with
  129. * {@link ContextProvider} is a workaround for the
  130. * i18n context becoming undefined as modal gets rendered
  131. * outside of the normal react app context.
  132. */
  133. }
  134. <ContextProvider i18n = { this.props.i18n }>
  135. <div
  136. onKeyDown = { this._onKeyDown }
  137. ref = { this._setDialogElement }>
  138. <form
  139. className = 'modal-dialog-form'
  140. id = 'modal-dialog-form'
  141. onSubmit = { this._onSubmit }>
  142. { children }
  143. </form>
  144. </div>
  145. </ContextProvider>
  146. </Modal>
  147. );
  148. }
  149. _onCancel: () => Function;
  150. /**
  151. * Returns a functional component to be used for the modal footer.
  152. *
  153. * @param {Object} options - The configuration for how the buttons in the
  154. * footer should display. Essentially {@code StatelessDialog} props should
  155. * be passed in.
  156. * @private
  157. * @returns {ReactElement}
  158. */
  159. _createFooterConstructor(options) {
  160. // Filter out falsy (null) values because {@code ButtonGroup} will error
  161. // if passed in anything but buttons with valid type props.
  162. const buttons = [
  163. this._renderOKButton(options),
  164. this._renderCancelButton(options)
  165. ].filter(Boolean);
  166. return function Footer(modalFooterProps) {
  167. return (
  168. <ModalFooter showKeyline = { modalFooterProps.showKeyline } >
  169. {
  170. /**
  171. * Atlaskit has this empty span (JustifySim) so...
  172. */
  173. }
  174. <span />
  175. <ButtonGroup>
  176. { buttons }
  177. </ButtonGroup>
  178. </ModalFooter>
  179. );
  180. };
  181. }
  182. _onCancel: () => void;
  183. /**
  184. * Dispatches action to hide the dialog.
  185. *
  186. * @returns {void}
  187. */
  188. _onCancel() {
  189. if (!this.props.isModal) {
  190. const { onCancel } = this.props;
  191. onCancel && onCancel();
  192. }
  193. }
  194. _onDialogDismissed: () => void;
  195. /**
  196. * Handles click on the blanket area.
  197. *
  198. * @returns {void}
  199. */
  200. _onDialogDismissed() {
  201. if (!this.props.disableBlanketClickDismiss) {
  202. this._onCancel();
  203. }
  204. }
  205. _onSubmit: (?string) => void;
  206. /**
  207. * Dispatches the action when submitting the dialog.
  208. *
  209. * @private
  210. * @param {string} value - The submitted value if any.
  211. * @returns {void}
  212. */
  213. _onSubmit(value) {
  214. const { onSubmit } = this.props;
  215. onSubmit && onSubmit(value);
  216. }
  217. /**
  218. * Renders Cancel button.
  219. *
  220. * @param {Object} options - The configuration for the Cancel button.
  221. * @param {boolean} options.cancelDisabled - True if the cancel button
  222. * should not be rendered.
  223. * @param {string} options.cancelTitleKey - The translation key to use as
  224. * text on the button.
  225. * @param {boolean} options.isModal - True if the cancel button should not
  226. * be rendered.
  227. * @private
  228. * @returns {ReactElement|null} The Cancel button if enabled and dialog is
  229. * not modal.
  230. */
  231. _renderCancelButton(options = {}) {
  232. if (options.cancelDisabled || options.isModal) {
  233. return null;
  234. }
  235. const {
  236. t /* The following fixes a flow error: */ = _.identity
  237. } = this.props;
  238. return (
  239. <Button
  240. appearance = 'subtle'
  241. id = { CANCEL_BUTTON_ID }
  242. key = 'cancel'
  243. onClick = { this._onCancel }
  244. type = 'button'>
  245. { t(options.cancelTitleKey || 'dialog.Cancel') }
  246. </Button>
  247. );
  248. }
  249. /**
  250. * Renders OK button.
  251. *
  252. * @param {Object} options - The configuration for the OK button.
  253. * @param {boolean} options.okDisabled - True if the button should display
  254. * as disabled and clicking should have no effect.
  255. * @param {string} options.okTitleKey - The translation key to use as text
  256. * on the button.
  257. * @param {boolean} options.submitDisabled - True if the button should not
  258. * be rendered.
  259. * @private
  260. * @returns {ReactElement|null} The OK button if enabled.
  261. */
  262. _renderOKButton(options = {}) {
  263. if (options.submitDisabled) {
  264. return null;
  265. }
  266. const {
  267. t /* The following fixes a flow error: */ = _.identity
  268. } = this.props;
  269. return (
  270. <Button
  271. appearance = 'primary'
  272. form = 'modal-dialog-form'
  273. id = { OK_BUTTON_ID }
  274. isDisabled = { options.okDisabled }
  275. key = 'submit'
  276. onClick = { this._onSubmit }
  277. type = 'button'>
  278. { t(options.okTitleKey || 'dialog.Ok') }
  279. </Button>
  280. );
  281. }
  282. _setDialogElement: (?HTMLElement) => void;
  283. /**
  284. * Sets the instance variable for the div containing the component's dialog
  285. * element so it can be accessed directly.
  286. *
  287. * @param {HTMLElement} element - The DOM element for the component's
  288. * dialog.
  289. * @private
  290. * @returns {void}
  291. */
  292. _setDialogElement(element: ?HTMLElement) {
  293. this._dialogElement = element;
  294. }
  295. _onKeyDown: (Object) => void;
  296. /**
  297. * Handles 'Enter' key in the dialog to submit/hide dialog depending on
  298. * the available buttons and their disabled state.
  299. *
  300. * @param {Object} event - The key event.
  301. * @private
  302. * @returns {void}
  303. */
  304. _onKeyDown(event) {
  305. // If the event coming to the dialog has been subject to preventDefault
  306. // we don't handle it here.
  307. if (event.defaultPrevented) {
  308. return;
  309. }
  310. if (event.key === 'Enter') {
  311. event.preventDefault();
  312. event.stopPropagation();
  313. if (this.props.submitDisabled && !this.props.cancelDisabled) {
  314. this._onCancel();
  315. } else if (!this.props.okDisabled) {
  316. this._onSubmit();
  317. }
  318. }
  319. }
  320. }
  321. export default translate(StatelessDialog);