mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ConfirmModal: Reuse confirm content (#88577)
This commit is contained in:
parent
b30c81b1ad
commit
03a000e1b5
@ -0,0 +1,140 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
|
import { useStyles2 } from '../../themes';
|
||||||
|
import { Button, ButtonVariant } from '../Button';
|
||||||
|
import { Input } from '../Input/Input';
|
||||||
|
import { Box } from '../Layout/Box/Box';
|
||||||
|
import { Stack } from '../Layout/Stack/Stack';
|
||||||
|
import { JustifyContent } from '../Layout/types';
|
||||||
|
import { ResponsiveProp } from '../Layout/utils/responsiveness';
|
||||||
|
|
||||||
|
export interface ConfirmContentProps {
|
||||||
|
/** Modal content */
|
||||||
|
body: React.ReactNode;
|
||||||
|
/** Modal description */
|
||||||
|
description?: React.ReactNode;
|
||||||
|
/** Text for confirm button */
|
||||||
|
confirmButtonLabel: string;
|
||||||
|
/** Confirm button variant */
|
||||||
|
confirmButtonVariant?: ButtonVariant;
|
||||||
|
/** Text user needs to fill in before confirming */
|
||||||
|
confirmPromptText?: string;
|
||||||
|
/** Text for dismiss button */
|
||||||
|
dismissButtonLabel?: string;
|
||||||
|
/** Variant for dismiss button */
|
||||||
|
dismissButtonVariant?: ButtonVariant;
|
||||||
|
/** Text for alternative button */
|
||||||
|
alternativeButtonLabel?: string;
|
||||||
|
/** Justify for buttons placement */
|
||||||
|
justifyButtons?: ResponsiveProp<JustifyContent>;
|
||||||
|
/** Confirm action callback
|
||||||
|
* Return a promise to disable the confirm button until the promise is resolved
|
||||||
|
*/
|
||||||
|
onConfirm(): void | Promise<void>;
|
||||||
|
/** Dismiss action callback */
|
||||||
|
onDismiss(): void;
|
||||||
|
/** Alternative action callback */
|
||||||
|
onAlternative?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmContent = ({
|
||||||
|
body,
|
||||||
|
confirmPromptText,
|
||||||
|
confirmButtonLabel,
|
||||||
|
confirmButtonVariant,
|
||||||
|
dismissButtonVariant,
|
||||||
|
dismissButtonLabel,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
onAlternative,
|
||||||
|
alternativeButtonLabel,
|
||||||
|
description,
|
||||||
|
justifyButtons = 'flex-end',
|
||||||
|
}: ConfirmContentProps) => {
|
||||||
|
const [disabled, setDisabled] = useState(Boolean(confirmPromptText));
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const onConfirmationTextChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
setDisabled(confirmPromptText?.toLowerCase().localeCompare(event.currentTarget.value.toLowerCase()) !== 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisabled(Boolean(confirmPromptText));
|
||||||
|
}, [confirmPromptText]);
|
||||||
|
|
||||||
|
const onConfirmClick = async () => {
|
||||||
|
setDisabled(true);
|
||||||
|
try {
|
||||||
|
await onConfirm();
|
||||||
|
} finally {
|
||||||
|
setDisabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handleSubmit } = useForm();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onConfirmClick)}>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{body}
|
||||||
|
{description ? <div className={styles.description}>{description}</div> : null}
|
||||||
|
{confirmPromptText ? (
|
||||||
|
<div className={styles.confirmationInput}>
|
||||||
|
<Stack alignItems="flex-start">
|
||||||
|
<Box>
|
||||||
|
<Input placeholder={`Type "${confirmPromptText}" to confirm`} onChange={onConfirmationTextChange} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonsContainer}>
|
||||||
|
<Stack justifyContent={justifyButtons} gap={2} wrap="wrap">
|
||||||
|
<Button variant={dismissButtonVariant} onClick={onDismiss} fill="outline">
|
||||||
|
{dismissButtonLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={confirmButtonVariant}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={buttonRef}
|
||||||
|
data-testid={selectors.pages.ConfirmModal.delete}
|
||||||
|
>
|
||||||
|
{confirmButtonLabel}
|
||||||
|
</Button>
|
||||||
|
{onAlternative ? (
|
||||||
|
<Button variant="primary" onClick={onAlternative}>
|
||||||
|
{alternativeButtonLabel}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
text: css({
|
||||||
|
fontSize: theme.typography.h5.fontSize,
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
}),
|
||||||
|
description: css({
|
||||||
|
fontSize: theme.typography.body.fontSize,
|
||||||
|
}),
|
||||||
|
confirmationInput: css({
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
buttonsContainer: css({
|
||||||
|
paddingTop: theme.spacing(3),
|
||||||
|
}),
|
||||||
|
});
|
@ -1,18 +1,14 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { IconName } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
|
|
||||||
import { useStyles2 } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
import { IconName } from '../../types/icon';
|
import { ButtonVariant } from '../Button';
|
||||||
import { Button, ButtonVariant } from '../Button';
|
|
||||||
import { Input } from '../Input/Input';
|
|
||||||
import { Box } from '../Layout/Box/Box';
|
|
||||||
import { Stack } from '../Layout/Stack/Stack';
|
|
||||||
import { Modal } from '../Modal/Modal';
|
import { Modal } from '../Modal/Modal';
|
||||||
|
|
||||||
|
import { ConfirmContent } from './ConfirmContent';
|
||||||
|
|
||||||
export interface ConfirmModalProps {
|
export interface ConfirmModalProps {
|
||||||
/** Toggle modal's open/closed state */
|
/** Toggle modal's open/closed state */
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -68,89 +64,29 @@ export const ConfirmModal = ({
|
|||||||
onAlternative,
|
onAlternative,
|
||||||
confirmButtonVariant = 'destructive',
|
confirmButtonVariant = 'destructive',
|
||||||
}: ConfirmModalProps): JSX.Element => {
|
}: ConfirmModalProps): JSX.Element => {
|
||||||
const [disabled, setDisabled] = useState(Boolean(confirmationText));
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const onConfirmationTextChange = (event: React.FormEvent<HTMLInputElement>) => {
|
|
||||||
setDisabled(confirmationText?.toLowerCase().localeCompare(event.currentTarget.value.toLowerCase()) !== 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// for some reason autoFocus property did no work on this button, but this does
|
|
||||||
if (isOpen) {
|
|
||||||
buttonRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
setDisabled(Boolean(confirmationText));
|
|
||||||
}
|
|
||||||
}, [isOpen, confirmationText]);
|
|
||||||
|
|
||||||
const onConfirmClick = async () => {
|
|
||||||
setDisabled(true);
|
|
||||||
try {
|
|
||||||
await onConfirm();
|
|
||||||
} finally {
|
|
||||||
setDisabled(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { handleSubmit } = useForm();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
<Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
||||||
<form onSubmit={handleSubmit(onConfirmClick)}>
|
<ConfirmContent
|
||||||
<div className={styles.modalText}>
|
body={body}
|
||||||
{body}
|
description={description}
|
||||||
{description ? <div className={styles.modalDescription}>{description}</div> : null}
|
confirmButtonLabel={confirmText}
|
||||||
{confirmationText ? (
|
dismissButtonLabel={dismissText}
|
||||||
<div className={styles.modalConfirmationInput}>
|
dismissButtonVariant={dismissVariant}
|
||||||
<Stack alignItems="flex-start">
|
confirmPromptText={confirmationText}
|
||||||
<Box>
|
alternativeButtonLabel={alternativeText}
|
||||||
<Input placeholder={`Type "${confirmationText}" to confirm`} onChange={onConfirmationTextChange} />
|
confirmButtonVariant={confirmButtonVariant}
|
||||||
</Box>
|
onConfirm={onConfirm}
|
||||||
</Stack>
|
onDismiss={onDismiss}
|
||||||
</div>
|
onAlternative={onAlternative}
|
||||||
) : null}
|
/>
|
||||||
</div>
|
|
||||||
<Modal.ButtonRow>
|
|
||||||
<Button variant={dismissVariant} onClick={onDismiss} fill="outline">
|
|
||||||
{dismissText}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant={confirmButtonVariant}
|
|
||||||
disabled={disabled}
|
|
||||||
ref={buttonRef}
|
|
||||||
data-testid={selectors.pages.ConfirmModal.delete}
|
|
||||||
>
|
|
||||||
{confirmText}
|
|
||||||
</Button>
|
|
||||||
{onAlternative ? (
|
|
||||||
<Button variant="primary" onClick={onAlternative}>
|
|
||||||
{alternativeText}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Modal.ButtonRow>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = () => ({
|
||||||
modal: css({
|
modal: css({
|
||||||
width: '500px',
|
width: '500px',
|
||||||
}),
|
}),
|
||||||
modalText: css({
|
|
||||||
fontSize: theme.typography.h5.fontSize,
|
|
||||||
color: theme.colors.text.primary,
|
|
||||||
}),
|
|
||||||
modalDescription: css({
|
|
||||||
fontSize: theme.typography.body.fontSize,
|
|
||||||
}),
|
|
||||||
modalConfirmationInput: css({
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user