diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx index c88bbea814f03..de01e14cc4419 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx @@ -2,6 +2,23 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { render } from "testHelpers/renderHelpers"; import { DeleteDialog } from "./DeleteDialog"; +import { act } from "react-dom/test-utils"; + +const inputTestId = "delete-dialog-name-confirmation"; + +async function fillInputField(inputElement: HTMLElement, text: string) { + // 2023-10-06 - There's something wonky with MUI's ConfirmDialog that causes + // its state to update after a typing event gets fired, and React Testing + // Library isn't able to catch it, making React DOM freak out because an + // "unexpected" state change happened. It won't fail the test, but it makes + // the console look really scary because it'll spit out a big warning message. + // Tried everything under the sun to catch the state changes the proper way, + // but the only way to get around it for now might be to manually make React + // DOM aware of the changes + + // eslint-disable-next-line testing-library/no-unnecessary-act -- have to make sure state updates don't slip through cracks + return act(() => userEvent.type(inputElement, text)); +} describe("DeleteDialog", () => { it("disables confirm button when the text field is empty", () => { @@ -14,6 +31,7 @@ describe("DeleteDialog", () => { name="MyTemplate" />, ); + const confirmButton = screen.getByRole("button", { name: "Delete" }); expect(confirmButton).toBeDisabled(); }); @@ -28,8 +46,10 @@ describe("DeleteDialog", () => { name="MyTemplate" />, ); - const textField = screen.getByTestId("delete-dialog-name-confirmation"); - await userEvent.type(textField, "MyTemplateWrong"); + + const textField = screen.getByTestId(inputTestId); + await fillInputField(textField, "MyTemplateButWrong"); + const confirmButton = screen.getByRole("button", { name: "Delete" }); expect(confirmButton).toBeDisabled(); }); @@ -44,8 +64,10 @@ describe("DeleteDialog", () => { name="MyTemplate" />, ); - const textField = screen.getByTestId("delete-dialog-name-confirmation"); - await userEvent.type(textField, "MyTemplate"); + + const textField = screen.getByTestId(inputTestId); + await fillInputField(textField, "MyTemplate"); + const confirmButton = screen.getByRole("button", { name: "Delete" }); expect(confirmButton).not.toBeDisabled(); }); diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx index 89fef1df95fe1..ab1134bb6a397 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx @@ -1,6 +1,13 @@ -import makeStyles from "@mui/styles/makeStyles"; +import { + type FC, + type FormEvent, + type PropsWithChildren, + useId, + useState, +} from "react"; + +import { useTheme } from "@emotion/react"; import TextField from "@mui/material/TextField"; -import { ChangeEvent, useState, PropsWithChildren, FC } from "react"; import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog"; export interface DeleteDialogProps { @@ -22,51 +29,23 @@ export const DeleteDialog: FC> = ({ name, confirmLoading, }) => { - const styles = useStyles(); - const [nameValue, setNameValue] = useState(""); - const confirmed = name === nameValue; - const handleChange = (event: ChangeEvent) => { - setNameValue(event.target.value); - }; - const hasError = nameValue.length > 0 && !confirmed; + const hookId = useId(); + const theme = useTheme(); - const content = ( - <> -

Deleting this {entity} is irreversible!

- {Boolean(info) &&

{info}

} -

Are you sure you want to proceed?

-

- Type “{name}” below to confirm. -

+ const [userConfirmationText, setUserConfirmationText] = useState(""); + const [isFocused, setIsFocused] = useState(false); -
{ - e.preventDefault(); - if (confirmed) { - onConfirm(); - } - }} - > - - - - ); + const deletionConfirmed = name === userConfirmationText; + const onSubmit = (event: FormEvent) => { + event.preventDefault(); + if (deletionConfirmed) { + onConfirm(); + } + }; + + const hasError = !deletionConfirmed && userConfirmationText.length > 0; + const displayErrorMessage = hasError && !isFocused; + const inputColor = hasError ? "error" : "primary"; return ( > = ({ title={`Delete ${entity}`} onConfirm={onConfirm} onClose={onCancel} - description={content} confirmLoading={confirmLoading} - disabled={!confirmed} + disabled={!deletionConfirmed} + description={ + <> +

Deleting this {entity} is irreversible!

+ + {Boolean(info) && ( +

{info}

+ )} + +

Are you sure you want to proceed?

+ +

+ Type “{name}” below to confirm. +

+ +
+ setUserConfirmationText(event.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + label={`Name of the ${entity} to delete`} + color={inputColor} + error={displayErrorMessage} + helperText={ + displayErrorMessage && + `${userConfirmationText} does not match the name of this ${entity}` + } + InputProps={{ color: inputColor }} + inputProps={{ + "data-testid": "delete-dialog-name-confirmation", + }} + /> + + + } /> ); }; - -const useStyles = makeStyles((theme) => ({ - warning: { - color: theme.palette.warning.light, - }, - - textField: { - marginTop: theme.spacing(3), - }, -}));