123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- import {cloneElement, Component, Fragment, isValidElement} from 'react';
- import {ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
- import Button, {ButtonProps} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import {t} from 'sentry/locale';
- export type ConfirmMessageRenderProps = {
- /**
- * Closes the modal, if `bypass` is true, will call `onConfirm` callback
- */
- close: (e: React.MouseEvent) => void;
- /**
- * Confirms the modal
- */
- confirm: () => 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;
- };
- export type ConfirmButtonsRenderProps = {
- /**
- * Applications can call this function to manually close the modal.
- */
- closeModal: () => void;
- /**
- * The default onClick behavior, including closing the modal and triggering the
- * onConfirm / onCancel callbacks.
- */
- defaultOnClick: () => void;
- };
- type ChildrenRenderProps = {
- open: () => void;
- };
- export type OpenConfirmOptions = {
- /**
- * If true, will skip the confirmation modal and call `onConfirm` callback
- */
- bypass?: boolean;
- /**
- * Text to show in the cancel button
- */
- cancelText?: React.ReactNode;
- /**
- * Text to show in the confirmation button
- */
- confirmText?: React.ReactNode;
- /**
- * 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;
- /**
- * Header of modal
- */
- header?: React.ReactNode;
- /**
- * Message to display to user when asking for confirmation
- */
- message?: React.ReactNode;
- /**
- * User cancels the modal
- */
- onCancel?: () => void;
- /**
- * Callback when user confirms
- */
- onConfirm?: () => void;
- /**
- * Callback function when user is in the confirming state called when the
- * confirm modal is opened
- */
- onConfirming?: () => void;
- /**
- * Button priority
- */
- priority?: ButtonProps['priority'];
- /**
- * Custom function to render the cancel button
- */
- renderCancelButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode;
- /**
- * Custom function to render the confirm button
- */
- renderConfirmButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode;
- /**
- * Used to render a message instead of using the static `message` prop.
- */
- renderMessage?: (renderProps: ConfirmMessageRenderProps) => React.ReactNode;
- };
- interface Props extends OpenConfirmOptions {
- /**
- * 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;
- /**
- * Stop event propagation when opening the confirm modal
- */
- stopPropagation?: boolean;
- }
- /**
- * Opens a confirmation modal when called. The procedural version of the
- * `Confirm` component
- */
- export const openConfirmModal = ({
- bypass,
- onConfirming,
- priority = 'primary',
- cancelText = t('Cancel'),
- confirmText = t('Confirm'),
- disableConfirmButton = false,
- ...rest
- }: OpenConfirmOptions) => {
- if (bypass) {
- rest.onConfirm?.();
- return;
- }
- const modalProps = {
- ...rest,
- priority,
- confirmText,
- cancelText,
- disableConfirmButton,
- };
- onConfirming?.();
- openModal(renderProps => <ConfirmModal {...renderProps} {...modalProps} />);
- };
- /**
- * The confirm component is somewhat special in that you can wrap any
- * onClick-able element with this to trigger a interstitial confirmation modal.
- *
- * This is the declarative alternative to using openConfirmModal
- */
- function Confirm({
- disabled,
- children,
- stopPropagation = false,
- ...openConfirmOptions
- }: Props) {
- const triggerModal = (e?: React.MouseEvent) => {
- if (stopPropagation) {
- e?.stopPropagation();
- }
- if (disabled) {
- return;
- }
- openConfirmModal(openConfirmOptions);
- };
- if (typeof children === 'function') {
- return children({open: triggerModal});
- }
- if (!isValidElement(children)) {
- return null;
- }
- // TODO(ts): Understand why the return type of `cloneElement` is strange
- return cloneElement(children, {disabled, onClick: triggerModal}) as any;
- }
- type ModalProps = ModalRenderProps &
- Pick<
- Props,
- | 'priority'
- | 'renderMessage'
- | 'renderConfirmButton'
- | 'renderCancelButton'
- | 'message'
- | 'confirmText'
- | 'cancelText'
- | 'header'
- | 'onConfirm'
- | 'onCancel'
- | 'disableConfirmButton'
- >;
- type ModalState = {
- /**
- * The callback registered from the rendered message to call
- */
- confirmCallback: null | (() => void);
- /**
- * Is confirm button disabled
- */
- disableConfirmButton: boolean;
- };
- class ConfirmModal extends 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 (isValidElement(message)) {
- return message;
- }
- return (
- <p>
- <strong>{message}</strong>
- </p>
- );
- }
- render() {
- const {
- Header,
- Body,
- Footer,
- priority,
- confirmText,
- cancelText,
- header,
- renderConfirmButton,
- renderCancelButton,
- } = this.props;
- return (
- <Fragment>
- {header && <Header>{header}</Header>}
- <Body>{this.confirmMessage}</Body>
- <Footer>
- <ButtonBar gap={2}>
- {renderCancelButton ? (
- renderCancelButton({
- closeModal: this.props.closeModal,
- defaultOnClick: this.handleClose,
- })
- ) : (
- <Button
- onClick={this.handleClose}
- aria-label={typeof cancelText === 'string' ? cancelText : t('Cancel')}
- >
- {cancelText ?? t('Cancel')}
- </Button>
- )}
- {renderConfirmButton ? (
- renderConfirmButton({
- closeModal: this.props.closeModal,
- defaultOnClick: this.handleConfirm,
- })
- ) : (
- <Button
- data-test-id="confirm-button"
- disabled={this.state.disableConfirmButton}
- priority={priority}
- onClick={this.handleConfirm}
- autoFocus
- aria-label={typeof confirmText === 'string' ? confirmText : t('Confirm')}
- >
- {confirmText ?? t('Confirm')}
- </Button>
- )}
- </ButtonBar>
- </Footer>
- </Fragment>
- );
- }
- }
- export default Confirm;
|