123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- import * as React from 'react';
- import {ModalRenderProps, openModal} from 'app/actionCreators/modal';
- import Button from 'app/components/button';
- import ButtonBar from 'app/components/buttonBar';
- import {t} from 'app/locale';
- export type ConfirmMessageRenderProps = {
- /**
- * Confirms the modal
- */
- confirm: () => void;
- /**
- * Closes the modal, if `bypass` is true, will call `onConfirm` callback
- */
- close: (e: React.MouseEvent) => void;
- /**
- * Set the disabled state of the confirm button
- */
- disableConfirmButton: (disable: boolean) => void;
- /**
- * When the modal is confirmed the function registered will be called.
- *
- * Useful if your rendered message contains some functionality that should be
- * triggered upon the modal being confirmed.
- *
- * This should be called in the components componentDidMount.
- */
- setConfirmCallback: (cb: () => void) => void;
- };
- type ChildrenRenderProps = {
- open: () => void;
- };
- type Props = {
- /**
- * Callback when user confirms
- */
- onConfirm?: () => void;
- /**
- * If true, will skip the confirmation modal and call `onConfirm` callback
- */
- bypass?: boolean;
- /**
- * Message to display to user when asking for confirmation
- */
- message?: React.ReactNode;
- /**
- * Used to render a message instead of using the static `message` prop.
- */
- renderMessage?: (renderProps: ConfirmMessageRenderProps) => React.ReactNode;
- /**
- * Render props to control rendering of the modal in its entirety
- */
- children?:
- | ((renderProps: ChildrenRenderProps) => React.ReactNode)
- | React.ReactElement<{disabled: boolean; onClick: (e: React.MouseEvent) => void}>;
- /**
- * Passed to `children` render function
- */
- disabled?: boolean;
- /**
- * Callback function when user is in the confirming state called when the
- * confirm modal is opened
- */
- onConfirming?: () => void;
- /**
- * User cancels the modal
- */
- onCancel?: () => void;
- /**
- * Header of modal
- */
- header?: React.ReactNode;
- /**
- * Stop event propagation when opening the confirm modal
- */
- stopPropagation?: boolean;
- /**
- * Disables the confirm button.
- *
- * XXX: Once the modal has been opened mutating this property will _not_
- * propagate into the modal.
- *
- * If you need the confirm buttons disabled state to be reactively
- * controlled, consider using the renderMessage prop, which receives a
- * `disableConfirmButton` function that you may use to control the state of it.
- */
- disableConfirmButton?: boolean;
- /**
- * Button priority
- */
- priority?: React.ComponentProps<typeof Button>['priority'];
- /**
- * Text to show in the cancel button
- */
- cancelText?: React.ReactNode;
- /**
- * Text to show in the confirmation button
- */
- confirmText?: React.ReactNode;
- };
- function Confirm({
- bypass,
- renderMessage,
- message,
- header,
- disabled,
- children,
- onConfirm,
- onConfirming,
- onCancel,
- priority = 'primary',
- cancelText = t('Cancel'),
- confirmText = t('Confirm'),
- stopPropagation = false,
- disableConfirmButton = false,
- }: Props) {
- const triggerModal = (e?: React.MouseEvent) => {
- if (stopPropagation) {
- e?.stopPropagation();
- }
- if (disabled) {
- return;
- }
- if (bypass) {
- onConfirm?.();
- return;
- }
- onConfirming?.();
- const modalProps = {
- priority,
- renderMessage,
- message,
- confirmText,
- cancelText,
- header,
- onConfirm,
- onCancel,
- disableConfirmButton,
- };
- openModal(renderProps => <ConfirmModal {...renderProps} {...modalProps} />);
- };
- if (typeof children === 'function') {
- return children({open: triggerModal});
- }
- if (!React.isValidElement(children)) {
- return null;
- }
- // TODO(ts): Understand why the return type of `cloneElement` is strange
- return React.cloneElement(children, {disabled, onClick: triggerModal}) as any;
- }
- type ModalProps = ModalRenderProps &
- Pick<
- Props,
- | 'priority'
- | 'renderMessage'
- | 'message'
- | 'confirmText'
- | 'cancelText'
- | 'header'
- | 'onConfirm'
- | 'onCancel'
- | 'disableConfirmButton'
- >;
- type ModalState = {
- /**
- * Is confirm button disabled
- */
- disableConfirmButton: boolean;
- /**
- * The callback registered from the rendered message to call
- */
- confirmCallback: null | (() => void);
- };
- class ConfirmModal extends React.Component<ModalProps, ModalState> {
- state: ModalState = {
- disableConfirmButton: !!this.props.disableConfirmButton,
- confirmCallback: null,
- };
- confirming: boolean = false;
- handleClose = () => {
- const {disableConfirmButton, onCancel, closeModal} = this.props;
- onCancel?.();
- this.setState({disableConfirmButton: disableConfirmButton ?? false});
- // always reset `confirming` when modal visibility changes
- this.confirming = false;
- closeModal();
- };
- handleConfirm = () => {
- const {onConfirm, closeModal} = this.props;
- // `confirming` is used to ensure `onConfirm` or the confirm callback is
- // only called once
- if (!this.confirming) {
- onConfirm?.();
- this.state.confirmCallback?.();
- }
- this.setState({disableConfirmButton: true});
- this.confirming = true;
- closeModal();
- };
- get confirmMessage() {
- const {message, renderMessage} = this.props;
- if (typeof renderMessage === 'function') {
- return renderMessage({
- confirm: this.handleConfirm,
- close: this.handleClose,
- disableConfirmButton: (state: boolean) =>
- this.setState({disableConfirmButton: state}),
- setConfirmCallback: (confirmCallback: () => void) =>
- this.setState({confirmCallback}),
- });
- }
- if (React.isValidElement(message)) {
- return message;
- }
- return (
- <p>
- <strong>{message}</strong>
- </p>
- );
- }
- render() {
- const {Header, Body, Footer, priority, confirmText, cancelText, header} = this.props;
- return (
- <React.Fragment>
- {header && <Header>{header}</Header>}
- <Body>{this.confirmMessage}</Body>
- <Footer>
- <ButtonBar gap={2}>
- <Button onClick={this.handleClose}>{cancelText}</Button>
- <Button
- data-test-id="confirm-button"
- disabled={this.state.disableConfirmButton}
- priority={priority}
- onClick={this.handleConfirm}
- autoFocus
- >
- {confirmText}
- </Button>
- </ButtonBar>
- </Footer>
- </React.Fragment>
- );
- }
- }
- export default Confirm;
|