diff --git a/services/ui-src/src/components/index.ts b/services/ui-src/src/components/index.ts index abe09ae5..5a947975 100644 --- a/services/ui-src/src/components/index.ts +++ b/services/ui-src/src/components/index.ts @@ -49,6 +49,8 @@ export { ExportedReportPage } from "./pages/Export/ExportedReportPage"; // menus export { Menu } from "./menus/Menu"; export { MenuOption } from "./menus/MenuOption"; +// modals +export { Modal } from "./modals/Modal"; // Redirects export { PostLogoutRedirect } from "./PostLogoutRedirect/index"; // tables diff --git a/services/ui-src/src/components/modals/Modal.test.tsx b/services/ui-src/src/components/modals/Modal.test.tsx new file mode 100644 index 00000000..2aa78aff --- /dev/null +++ b/services/ui-src/src/components/modals/Modal.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { axe } from "jest-axe"; +//components +import { Text } from "@chakra-ui/react"; +import { Modal } from "components"; + +const mockCloseHandler = jest.fn(); +const mockConfirmationHandler = jest.fn(); + +const content = { + heading: "Dialog Heading", + body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan diam vitae metus lacinia, eget tempor purus placerat.", + actionButtonText: "Dialog Action", + closeButtonText: "Cancel", +}; + +const modalComponent = ( + + {content.body} + +); + +describe("Test Modal", () => { + beforeEach(() => { + render(modalComponent); + }); + + test("Modal shows the contents", () => { + expect(screen.getByText(content.heading)).toBeTruthy(); + expect(screen.getByText(content.body)).toBeTruthy(); + }); + + test("Modals action button can be clicked", () => { + fireEvent.click(screen.getByText(/Dialog Action/i)); + expect(mockConfirmationHandler).toHaveBeenCalledTimes(1); + }); + + test("Modals close button can be clicked", () => { + fireEvent.click(screen.getByText(/Cancel/i)); + expect(mockCloseHandler).toHaveBeenCalledTimes(1); + }); +}); + +describe("Test Modal accessibility", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(modalComponent); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/services/ui-src/src/components/modals/Modal.tsx b/services/ui-src/src/components/modals/Modal.tsx new file mode 100644 index 00000000..f90f7c55 --- /dev/null +++ b/services/ui-src/src/components/modals/Modal.tsx @@ -0,0 +1,183 @@ +import { ReactNode } from "react"; +import { + Box, + Button, + Flex, + Heading, + Image, + Modal as ChakraModal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spinner, +} from "@chakra-ui/react"; +import closeIcon from "assets/icons/close/icon_close_primary.svg"; + +export const Modal = ({ + modalDisclosure, + content, + onConfirmHandler, + submitting, + formId, + children, +}: Props) => { + return ( + + + + + + {content.heading} + + + {content.subheading && ( + {content.subheading} + )} + + + + {children} + + {formId && ( + + )} + {onConfirmHandler && ( + + )} + {content.closeButtonText && ( + + )} + + + + ); +}; + +interface Props { + modalDisclosure: { + isOpen: boolean; + onClose: any; + }; + content: { + heading: string; + subheading?: string; + actionButtonText: string | ReactNode; + closeButtonText?: string; + }; + submitting?: boolean; + onConfirmHandler?: Function; + formId?: string; + children?: ReactNode; + [key: string]: any; +} + +const sx = { + modalContent: { + boxShadow: ".125rem .125rem .25rem", + borderRadius: "0", + maxWidth: "30rem", + marginX: "4rem", + padding: "2rem", + }, + modalHeader: { + padding: "0", + }, + modalHeaderText: { + padding: "0 4rem 0 0", + fontSize: "2xl", + fontWeight: "bold", + }, + modalSubheader: { + margin: "0.5rem auto -1rem auto", + }, + modalCloseContainer: { + alignItems: "center", + justifycontent: "center", + flexShrink: "0", + position: "absolute", + top: "2rem", + right: "2rem", + }, + modalClose: { + span: { + margin: "0.25rem", + paddingTop: "0.06rem", + svg: { + fontSize: "xs", + width: "xs", + height: "xs", + }, + }, + }, + modalBody: { + paddingX: "0", + paddingY: "1rem", + }, + modalFooter: { + justifyContent: "flex-start", + padding: "0", + paddingTop: "2rem", + }, + action: { + justifyContent: "center", + marginRight: "2rem", + minWidth: "10rem", + span: { + marginLeft: "0.5rem", + marginRight: "-0.25rem", + "&.ds-c-spinner": { + marginLeft: 0, + }, + }, + ".mobile &": { + fontSize: "sm", + }, + }, + close: { + justifyContent: "start", + padding: ".5rem 1rem", + span: { + marginLeft: "0rem", + marginRight: "0.5rem", + }, + ".mobile &": { + fontSize: "sm", + marginRight: "0", + }, + }, + closeIcon: { + width: "0.75rem", + }, +};