You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StatelessDialog.js 8.6KB

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