Skip to content

Commit

Permalink
Consolidate buttons (#2826)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Dec 7, 2023
1 parent 3e61ac7 commit 4e0e257
Show file tree
Hide file tree
Showing 22 changed files with 333 additions and 173 deletions.
25 changes: 25 additions & 0 deletions src/components/Buttons/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Close } from "@mui/icons-material";
import { IconButton } from "@mui/material";
import { CSSProperties, ReactElement } from "react";

interface CloseButtonProps {
close: () => void;
}

export default function CloseButton(props: CloseButtonProps): ReactElement {
const closeButtonStyle: CSSProperties = {
position: "absolute",
top: 0,
...(document.body.dir === "rtl" ? { left: 0 } : { right: 0 }),
};

return (
<IconButton
aria-label="close"
onClick={props.close}
style={closeButtonStyle}
>
<Close />
</IconButton>
);
}
45 changes: 45 additions & 0 deletions src/components/Buttons/DeleteButtonWithDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Delete } from "@mui/icons-material";
import { ReactElement, useState } from "react";

import { IconButtonWithTooltip } from "components/Buttons";
import { CancelConfirmDialog } from "components/Dialogs";

interface DeleteButtonWithDialogProps {
buttonId: string;
buttonIdCancel?: string;
buttonIdConfirm?: string;
delete: () => void | Promise<void>;
disabled?: boolean;
textId: string;
tooltipTextId?: string;
}

export default function DeleteButtonWithDialog(
props: DeleteButtonWithDialogProps
): ReactElement {
const [open, setOpen] = useState(false);

const handleConfirm = async (): Promise<void> => {
await props.delete();
setOpen(false);
};

return (
<>
<IconButtonWithTooltip
buttonId={props.buttonId}
icon={<Delete />}
onClick={props.disabled ? undefined : () => setOpen(true)}
textId={props.tooltipTextId || props.textId}
/>
<CancelConfirmDialog
buttonIdCancel={props.buttonIdCancel}
buttonIdConfirm={props.buttonIdConfirm}
handleCancel={() => setOpen(false)}
handleConfirm={handleConfirm}
open={open}
textId={props.textId}
/>
</>
);
}
2 changes: 1 addition & 1 deletion src/components/Buttons/FlagButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement {
}
text={text}
textId={active ? "flags.edit" : "flags.add"}
small
size="small"
onClick={
props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/Buttons/IconButtonWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Tooltip, IconButton } from "@mui/material";
import { ReactElement, ReactNode } from "react";
import { MouseEventHandler, ReactElement, ReactNode } from "react";
import { useTranslation } from "react-i18next";

interface IconButtonWithTooltipProps {
icon: ReactElement;
text?: ReactNode;
textId?: string;
small?: boolean;
onClick?: () => void;
size?: "large" | "medium" | "small";
onClick?: MouseEventHandler<HTMLButtonElement>;
buttonId: string;
side?: "bottom" | "left" | "right" | "top";
}
Expand All @@ -25,7 +25,7 @@ export default function IconButtonWithTooltip(
<span>
<IconButton
onClick={props.onClick}
size={props.small ? "small" : "medium"}
size={props.size || "medium"}
id={props.buttonId}
disabled={!props.onClick}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Buttons/PartOfSpeechButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function PartOfSpeech(props: PartOfSpeechProps): ReactElement {
icon={<Hexagon fontSize="small" sx={{ color }} />}
onClick={props.onClick}
side="top"
small
size="small"
text={hoverText}
/>
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/Buttons/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CloseButton from "components/Buttons/CloseButton";
import DeleteButtonWithDialog from "components/Buttons/DeleteButtonWithDialog";
import FileInputButton from "components/Buttons/FileInputButton";
import FlagButton from "components/Buttons/FlagButton";
import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip";
Expand All @@ -7,6 +9,8 @@ import PartOfSpeechButton from "components/Buttons/PartOfSpeechButton";
import UndoButton from "components/Buttons/UndoButton";

export {
CloseButton,
DeleteButtonWithDialog,
FileInputButton,
FlagButton,
IconButtonWithTooltip,
Expand Down
85 changes: 85 additions & 0 deletions src/components/Buttons/tests/DeleteButtonWithDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ReactTestRenderer, act, create } from "react-test-renderer";

import "tests/reactI18nextMock";

import DeleteButtonWithDialog from "components/Buttons/DeleteButtonWithDialog";
import { CancelConfirmDialog } from "components/Dialogs";

// Dialog uses portals, which are not supported in react-test-renderer.
jest.mock("@mui/material", () => {
const materialUiCore = jest.requireActual("@mui/material");
return {
...jest.requireActual("@mui/material"),
Dialog: materialUiCore.Container,
};
});

const mockDelete = jest.fn();
const buttonId = "button-id";
const buttonIdCancel = "button-id-cancel";
const buttonIdConfirm = "button-id-confirm";
const textId = "text-id";

let cellHandle: ReactTestRenderer;

const renderDeleteCell = async (): Promise<void> => {
await act(async () => {
cellHandle = create(
<DeleteButtonWithDialog
buttonId={buttonId}
buttonIdCancel={buttonIdCancel}
buttonIdConfirm={buttonIdConfirm}
delete={mockDelete}
textId={textId}
/>
);
});
};

beforeEach(async () => {
jest.clearAllMocks();
await renderDeleteCell();
});

describe("DeleteCell", () => {
it("has working dialog buttons", async () => {
const dialog = cellHandle.root.findByType(CancelConfirmDialog);
const deleteButton = cellHandle.root.findByProps({ id: buttonId });

expect(dialog.props.open).toBeFalsy();
await act(async () => {
deleteButton.props.onClick();
});
expect(dialog.props.open).toBeTruthy();
const cancelButton = cellHandle.root.findByProps({ id: buttonIdCancel });
await act(async () => {
cancelButton.props.onClick();
});
expect(dialog.props.open).toBeFalsy();
await act(async () => {
deleteButton.props.onClick();
});
expect(dialog.props.open).toBeTruthy();
const confButton = cellHandle.root.findByProps({ id: buttonIdConfirm });
await act(async () => {
await confButton.props.onClick();
});
expect(dialog.props.open).toBeFalsy();
});

it("only deletes after confirmation", async () => {
const deleteButton = cellHandle.root.findByProps({ id: buttonId });

await act(async () => {
deleteButton.props.onClick();
cellHandle.root.findByProps({ id: buttonIdCancel }).props.onClick();
deleteButton.props.onClick();
});
expect(mockDelete).not.toHaveBeenCalled();
const confButton = cellHandle.root.findByProps({ id: buttonIdConfirm });
await act(async () => {
await confButton.props.onClick();
});
expect(mockDelete).toHaveBeenCalled();
});
});
10 changes: 2 additions & 8 deletions src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Close } from "@mui/icons-material";
import {
Dialog,
DialogContent,
Grid,
IconButton,
MenuList,
Typography,
} from "@mui/material";
import { ReactElement } from "react";
import { useTranslation } from "react-i18next";

import { GramCatGroup, Sense, Word } from "api/models";
import { CloseButton } from "components/Buttons";
import StyledMenuItem from "components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem";
import {
DomainCell,
Expand Down Expand Up @@ -107,12 +106,7 @@ export function SenseList(props: SenseListProps): ReactElement {
return (
<>
{/* Cancel button */}
<IconButton
onClick={() => props.closeDialog()}
style={{ position: "absolute", right: 0, top: 0 }}
>
<Close />
</IconButton>
<CloseButton close={() => props.closeDialog()} />
{/* Header */}
<Typography variant="h3">{t("addWords.selectSense")}</Typography>
{/* Sense options */}
Expand Down
10 changes: 2 additions & 8 deletions src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Close } from "@mui/icons-material";
import {
Dialog,
DialogContent,
Grid,
IconButton,
MenuList,
Typography,
} from "@mui/material";
import { ReactElement } from "react";
import { useTranslation } from "react-i18next";

import { GramCatGroup, Word } from "api/models";
import { CloseButton } from "components/Buttons";
import StyledMenuItem from "components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem";
import {
DomainCell,
Expand Down Expand Up @@ -108,12 +107,7 @@ export function VernList(props: VernListProps): ReactElement {
return (
<>
{/* Cancel button */}
<IconButton
onClick={() => props.closeDialog()}
style={{ position: "absolute", right: 0, top: 0 }}
>
<Close />
</IconButton>
<CloseButton close={() => props.closeDialog()} />
{/* Header */}
<Typography variant="h3">{t("addWords.selectEntry")}</Typography>
{/* Entry options */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dialogs/DeleteEditTextDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function DeleteEditTextDialog(
<Tooltip title={t("buttons.cancel")} placement={"left"}>
<IconButton
size="small"
aria-label="cancel"
aria-label="close"
onClick={onCancel}
style={{ position: "absolute", right: 4, top: 4 }}
>
Expand Down
77 changes: 77 additions & 0 deletions src/components/Dialogs/UploadImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Grid, Typography } from "@mui/material";
import { FormEvent, ReactElement, useState } from "react";
import { useTranslation } from "react-i18next";

import { FileInputButton, LoadingDoneButton } from "components/Buttons";

interface ImageUploadProps {
doneCallback?: () => void;
uploadImage: (imgFile: File) => Promise<void>;
}

/**
* Allows the current user to select an image and upload it
*/
export default function ImageUpload(props: ImageUploadProps): ReactElement {
const [file, setFile] = useState<File>();
const [filename, setFilename] = useState<string>();
const [loading, setLoading] = useState<boolean>(false);
const [done, setDone] = useState<boolean>(false);
const { t } = useTranslation();

function updateFile(file: File): void {
if (file) {
setFile(file);
setFilename(file.name);
}
}

async function upload(e: FormEvent<EventTarget>): Promise<void> {
e.preventDefault();
e.stopPropagation();
if (file) {
setLoading(true);
await props
.uploadImage(file)
.then(onDone)
.catch(() => setLoading(false));
}
}

async function onDone(): Promise<void> {
setDone(true);
if (props.doneCallback) {
setTimeout(props.doneCallback, 500);
}
}

return (
<form onSubmit={(e) => upload(e)}>
{/* Displays the name of the selected file */}
{filename && (
<Typography variant="body1" noWrap>
{t("createProject.fileSelected")}: {filename}
</Typography>
)}
<Grid container spacing={1} justifyContent="flex-start">
<Grid item>
<FileInputButton
updateFile={(file) => updateFile(file)}
accept="image/*"
>
{t("buttons.browse")}
</FileInputButton>
</Grid>
<Grid item>
<LoadingDoneButton
loading={loading}
done={done}
buttonProps={{ type: "submit", id: "image-upload-save" }}
>
{t("buttons.save")}
</LoadingDoneButton>
</Grid>
</Grid>
</form>
);
}
30 changes: 30 additions & 0 deletions src/components/Dialogs/UploadImageDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { ReactElement } from "react";
import { useTranslation } from "react-i18next";

import UploadImage from "components/Dialogs/UploadImage";

interface UploadImageDialogProps {
close: () => void;
open: boolean;
titleId: string;
uploadImage: (imageFile: File) => Promise<void>;
}

export default function UploadImageDialog(
props: UploadImageDialogProps
): ReactElement {
const { t } = useTranslation();

return (
<Dialog onClose={props.close} open={props.open}>
<DialogTitle>{t(props.titleId)}</DialogTitle>
<DialogContent>
<UploadImage
doneCallback={props.close}
uploadImage={props.uploadImage}
/>
</DialogContent>
</Dialog>
);
}
Loading

0 comments on commit 4e0e257

Please sign in to comment.