confirm.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import * as React from 'react';
  2. import {ModalRenderProps, openModal} from 'app/actionCreators/modal';
  3. import Button from 'app/components/button';
  4. import ButtonBar from 'app/components/buttonBar';
  5. import {t} from 'app/locale';
  6. export type ConfirmMessageRenderProps = {
  7. /**
  8. * Confirms the modal
  9. */
  10. confirm: () => void;
  11. /**
  12. * Closes the modal, if `bypass` is true, will call `onConfirm` callback
  13. */
  14. close: (e: React.MouseEvent) => void;
  15. /**
  16. * Set the disabled state of the confirm button
  17. */
  18. disableConfirmButton: (disable: boolean) => void;
  19. /**
  20. * When the modal is confirmed the function registered will be called.
  21. *
  22. * Useful if your rendered message contains some functionality that should be
  23. * triggered upon the modal being confirmed.
  24. *
  25. * This should be called in the components componentDidMount.
  26. */
  27. setConfirmCallback: (cb: () => void) => void;
  28. selectedValue?: any;
  29. };
  30. export type ConfirmButtonsRenderProps = {
  31. /**
  32. * Applications can call this function to manually close the modal.
  33. */
  34. closeModal: () => void;
  35. /**
  36. * The default onClick behavior, including closing the modal and triggering the
  37. * onConfirm / onCancel callbacks.
  38. */
  39. defaultOnClick: () => void;
  40. };
  41. type ChildrenRenderProps = {
  42. open: (e?: React.MouseEvent, selectedValue?: string) => void;
  43. };
  44. export type OpenConfirmOptions = {
  45. /**
  46. * Callback when user confirms
  47. */
  48. onConfirm?: (selectedValue?: any) => void;
  49. /**
  50. * Custom function to render the confirm button
  51. */
  52. renderConfirmButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode;
  53. /**
  54. * Custom function to render the cancel button
  55. */
  56. renderCancelButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode;
  57. /**
  58. * If true, will skip the confirmation modal and call `onConfirm` callback
  59. */
  60. bypass?: boolean;
  61. /**
  62. * Message to display to user when asking for confirmation
  63. */
  64. message?: React.ReactNode;
  65. /**
  66. * Used to render a message instead of using the static `message` prop.
  67. */
  68. renderMessage?: (renderProps: ConfirmMessageRenderProps) => React.ReactNode;
  69. /**
  70. * Callback function when user is in the confirming state called when the
  71. * confirm modal is opened
  72. */
  73. onConfirming?: () => void;
  74. /**
  75. * User cancels the modal
  76. */
  77. onCancel?: () => void;
  78. /**
  79. * Header of modal
  80. */
  81. header?: React.ReactNode;
  82. /**
  83. * Disables the confirm button.
  84. *
  85. * XXX: Once the modal has been opened mutating this property will _not_
  86. * propagate into the modal.
  87. *
  88. * If you need the confirm buttons disabled state to be reactively
  89. * controlled, consider using the renderMessage prop, which receives a
  90. * `disableConfirmButton` function that you may use to control the state of it.
  91. */
  92. disableConfirmButton?: boolean;
  93. /**
  94. * Button priority
  95. */
  96. priority?: React.ComponentProps<typeof Button>['priority'];
  97. /**
  98. * Text to show in the cancel button
  99. */
  100. cancelText?: React.ReactNode;
  101. /**
  102. * Text to show in the confirmation button
  103. */
  104. confirmText?: React.ReactNode;
  105. selectedValue?: string;
  106. };
  107. type Props = OpenConfirmOptions & {
  108. /**
  109. * Render props to control rendering of the modal in its entirety
  110. */
  111. children?:
  112. | ((renderProps: ChildrenRenderProps) => React.ReactNode)
  113. | React.ReactElement<{disabled: boolean; onClick: (e: React.MouseEvent) => void}>;
  114. /**
  115. * Passed to `children` render function
  116. */
  117. disabled?: boolean;
  118. /**
  119. * Stop event propagation when opening the confirm modal
  120. */
  121. stopPropagation?: boolean;
  122. };
  123. /**
  124. * Opens a confirmation modal when called. The procedural version of the
  125. * `Confirm` component
  126. */
  127. export const openConfirmModal = ({
  128. bypass,
  129. onConfirming,
  130. priority = 'primary',
  131. cancelText = t('Cancel'),
  132. confirmText = t('Confirm'),
  133. disableConfirmButton = false,
  134. ...rest
  135. }: OpenConfirmOptions) => {
  136. if (bypass) {
  137. rest.onConfirm?.();
  138. return;
  139. }
  140. const modalProps = {
  141. ...rest,
  142. priority,
  143. confirmText,
  144. cancelText,
  145. disableConfirmButton,
  146. };
  147. onConfirming?.();
  148. openModal(renderProps => <ConfirmModal {...renderProps} {...modalProps} />);
  149. };
  150. /**
  151. * The confirm component is somewhat special in that you can wrap any
  152. * onClick-able element with this to trigger a interstital confirmation modal.
  153. *
  154. * This is the declarative alternative to using openConfirmModal
  155. */
  156. function Confirm({
  157. disabled,
  158. children,
  159. stopPropagation = false,
  160. ...openConfirmOptions
  161. }: Props) {
  162. const triggerModal = (e?: React.MouseEvent, selectedValue?: string) => {
  163. if (stopPropagation) {
  164. e?.stopPropagation();
  165. }
  166. if (disabled) {
  167. return;
  168. }
  169. openConfirmModal({...openConfirmOptions, ...(selectedValue && {selectedValue})});
  170. };
  171. if (typeof children === 'function') {
  172. return children({open: triggerModal});
  173. }
  174. if (!React.isValidElement(children)) {
  175. return null;
  176. }
  177. // TODO(ts): Understand why the return type of `cloneElement` is strange
  178. return React.cloneElement(children, {disabled, onClick: triggerModal}) as any;
  179. }
  180. type ModalProps = ModalRenderProps &
  181. Pick<
  182. Props,
  183. | 'priority'
  184. | 'renderMessage'
  185. | 'renderConfirmButton'
  186. | 'renderCancelButton'
  187. | 'message'
  188. | 'confirmText'
  189. | 'cancelText'
  190. | 'header'
  191. | 'onConfirm'
  192. | 'onCancel'
  193. | 'disableConfirmButton'
  194. | 'selectedValue'
  195. >;
  196. type ModalState = {
  197. /**
  198. * Is confirm button disabled
  199. */
  200. disableConfirmButton: boolean;
  201. /**
  202. * The callback registered from the rendered message to call
  203. */
  204. confirmCallback: null | (() => void);
  205. };
  206. class ConfirmModal extends React.Component<ModalProps, ModalState> {
  207. state: ModalState = {
  208. disableConfirmButton: !!this.props.disableConfirmButton,
  209. confirmCallback: null,
  210. };
  211. confirming: boolean = false;
  212. handleClose = () => {
  213. const {disableConfirmButton, onCancel, closeModal} = this.props;
  214. onCancel?.();
  215. this.setState({disableConfirmButton: disableConfirmButton ?? false});
  216. // always reset `confirming` when modal visibility changes
  217. this.confirming = false;
  218. closeModal();
  219. };
  220. handleConfirm = () => {
  221. const {onConfirm, closeModal, selectedValue} = this.props;
  222. // `confirming` is used to ensure `onConfirm` or the confirm callback is
  223. // only called once
  224. if (!this.confirming) {
  225. onConfirm?.(selectedValue);
  226. this.state.confirmCallback?.();
  227. }
  228. this.setState({disableConfirmButton: true});
  229. this.confirming = true;
  230. closeModal();
  231. };
  232. get confirmMessage() {
  233. const {message, renderMessage, selectedValue} = this.props;
  234. if (typeof renderMessage === 'function') {
  235. return renderMessage({
  236. confirm: this.handleConfirm,
  237. close: this.handleClose,
  238. disableConfirmButton: (state: boolean) =>
  239. this.setState({disableConfirmButton: state}),
  240. setConfirmCallback: (confirmCallback: () => void) =>
  241. this.setState({confirmCallback}),
  242. selectedValue,
  243. });
  244. }
  245. if (React.isValidElement(message)) {
  246. return message;
  247. }
  248. return (
  249. <p>
  250. <strong>{message}</strong>
  251. </p>
  252. );
  253. }
  254. render() {
  255. const {
  256. Header,
  257. Body,
  258. Footer,
  259. priority,
  260. confirmText,
  261. cancelText,
  262. header,
  263. renderConfirmButton,
  264. renderCancelButton,
  265. } = this.props;
  266. return (
  267. <React.Fragment>
  268. {header && <Header>{header}</Header>}
  269. <Body>{this.confirmMessage}</Body>
  270. <Footer>
  271. <ButtonBar gap={2}>
  272. {renderCancelButton ? (
  273. renderCancelButton({
  274. closeModal: this.props.closeModal,
  275. defaultOnClick: this.handleClose,
  276. })
  277. ) : (
  278. <Button onClick={this.handleClose}>{cancelText}</Button>
  279. )}
  280. {renderConfirmButton ? (
  281. renderConfirmButton({
  282. closeModal: this.props.closeModal,
  283. defaultOnClick: this.handleConfirm,
  284. })
  285. ) : (
  286. <Button
  287. data-test-id="confirm-button"
  288. disabled={this.state.disableConfirmButton}
  289. priority={priority}
  290. onClick={this.handleConfirm}
  291. autoFocus
  292. >
  293. {confirmText}
  294. </Button>
  295. )}
  296. </ButtonBar>
  297. </Footer>
  298. </React.Fragment>
  299. );
  300. }
  301. }
  302. export default Confirm;