Skip to content

Commit

Permalink
feat(Modal): handle onClose and show logic of component (#2429)
Browse files Browse the repository at this point in the history
  • Loading branch information
YossiSaadi authored Sep 25, 2024
1 parent 50c187f commit 2e0a86d
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 28 deletions.
74 changes: 52 additions & 22 deletions packages/core/src/components/ModalNew/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import { getStyle } from "../../../helpers/typesciptCssModulesHelper";
import { camelCase } from "lodash-es";
import { ModalProvider } from "../context/ModalContext";
import { ModalContextProps } from "../context/ModalContext.types";
import useKeyEvent from "../../../hooks/useKeyEvent";
import { keyCodes } from "../../../constants";

const Modal = forwardRef(
(
{
id,
// Would be implemented in a later PR
// eslint-disable-next-line @typescript-eslint/no-unused-vars
show,
size = "medium",
renderHeaderAction,
closeButtonTheme,
closeButtonAriaLabel,
onClose,
onClose = () => {},
children,
className,
"data-testid": dataTestId
Expand All @@ -43,27 +43,57 @@ const Modal = forwardRef(
[id, setTitleIdCallback, setDescriptionIdCallback]
);

const onBackdropClick = useCallback<React.MouseEventHandler<HTMLDivElement>>(
e => {
if (!show) return;
onClose(e);
},
[onClose, show]
);

const onEscClick = useCallback<React.KeyboardEventHandler<HTMLBodyElement>>(
e => {
if (!show) return;
onClose(e);
},
[onClose, show]
);

useKeyEvent({
callback: onEscClick,
capture: true,
keys: [keyCodes.ESCAPE]
});

if (!show) {
return null;
}

return (
<ModalProvider value={contextValue}>
<div id="overlay" className={styles.overlay}>
<div
ref={ref}
className={cx(styles.modal, getStyle(styles, camelCase("size-" + size)), className)}
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL, id)}
role="dialog"
aria-modal
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
<ModalTopActions
renderAction={renderHeaderAction}
color={closeButtonTheme}
closeButtonAriaLabel={closeButtonAriaLabel}
onClose={onClose}
/>
{children}
</div>
<div
data-testid={getTestId(ComponentDefaultTestId.MODAL_NEXT_OVERLAY, id)}
className={styles.overlay}
onClick={onBackdropClick}
aria-hidden
/>
<div
ref={ref}
className={cx(styles.modal, getStyle(styles, camelCase("size-" + size)), className)}
id={id}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.MODAL_NEXT, id)}
role="dialog"
aria-modal
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
<ModalTopActions
renderAction={renderHeaderAction}
color={closeButtonTheme}
closeButtonAriaLabel={closeButtonAriaLabel}
onClose={onClose}
/>
{children}
</div>
</ModalProvider>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/components/ModalNew/Modal/Modal.types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { ModalTopActionsProps } from "../ModalTopActions/ModalTopActions.types";

export type ModalSize = "small" | "medium" | "large";

export type ModalCloseEvent =
| React.MouseEvent<HTMLDivElement | HTMLButtonElement>
| React.KeyboardEvent<HTMLBodyElement>;

export interface ModalProps extends VibeComponentProps {
id: string;
show: boolean;
size?: ModalSize;
closeButtonTheme?: ModalTopActionsProps["color"];
closeButtonAriaLabel?: ModalTopActionsProps["closeButtonAriaLabel"];
onClose?: ModalTopActionsProps["onClose"];
onClose?: (event: ModalCloseEvent) => void;
renderHeaderAction?: ModalTopActionsProps["renderAction"];
children: React.ReactNode;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Modal from "../Modal";

describe("Modal", () => {
Expand Down Expand Up @@ -27,6 +28,16 @@ describe("Modal", () => {
expect(getByTestId("modal")).toHaveAttribute("aria-modal", "true");
});

it("does not render when 'show' is false", () => {
const { queryByRole } = render(
<Modal id={id} show={false}>
{childrenContent}
</Modal>
);

expect(queryByRole("dialog")).not.toBeInTheDocument();
});

it("renders the children content correctly", () => {
const { getByText } = render(
<Modal id={id} show>
Expand Down Expand Up @@ -57,7 +68,7 @@ describe("Modal", () => {
expect(getByRole("dialog")).toHaveClass("sizeLarge");
});

it("calls onClose when the close button is clicked", () => {
it("calls onClose when the close button is clicked with mouse", () => {
const mockOnClose = jest.fn();
const { getByLabelText } = render(
<Modal id={id} show onClose={mockOnClose} closeButtonAriaLabel={closeButtonAriaLabel}>
Expand All @@ -69,7 +80,54 @@ describe("Modal", () => {
expect(mockOnClose).toHaveBeenCalled();
});

it.todo("does not render when 'show' is false");
it("calls onClose when the close button is clicked with keyboard", () => {
const mockOnClose = jest.fn();
const { getByLabelText } = render(
<Modal id={id} show onClose={mockOnClose} closeButtonAriaLabel={closeButtonAriaLabel}>
{childrenContent}
</Modal>
);

fireEvent.focus(getByLabelText(closeButtonAriaLabel));
userEvent.type(getByLabelText(closeButtonAriaLabel), "{space}");
expect(mockOnClose).toHaveBeenCalled();
});

it("calls onClose when the backdrop is clicked", () => {
const mockOnClose = jest.fn();
const { getByTestId } = render(
<Modal id={id} show onClose={mockOnClose}>
{childrenContent}
</Modal>
);

fireEvent.click(getByTestId("modal-overlay_" + id));
expect(mockOnClose).toHaveBeenCalled();
});

it("calls onClose when the Escape key is pressed while focused on dialog", () => {
const mockOnClose = jest.fn();
const { getByRole } = render(
<Modal id={id} show onClose={mockOnClose}>
{childrenContent}
</Modal>
);

fireEvent.keyDown(getByRole("dialog"), { key: "Escape" });
expect(mockOnClose).toHaveBeenCalled();
});

it("calls onClose when the Escape key is pressed without focus", () => {
const mockOnClose = jest.fn();
render(
<Modal id={id} show onClose={mockOnClose}>
{childrenContent}
</Modal>
);

userEvent.keyboard("{Escape}");
expect(mockOnClose).toHaveBeenCalled();
});

it.todo("renders the correct aria-labelledby");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export interface ModalTopActionsProps {
| ((color?: ModalTopActionsButtonColor) => React.ReactElement<typeof MenuButton | typeof IconButton>);
color?: ModalTopActionsColor;
closeButtonAriaLabel?: string;
onClose?: React.MouseEventHandler<HTMLDivElement>;
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
4 changes: 2 additions & 2 deletions packages/core/src/tests/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ export enum ComponentDefaultTestId {
MODAL_CONTENT = "modal-content",
MODAL_HEADER = "modal-header",
MODAL_FOOTER_BUTTONS = "modal-footer-buttons",
MODAL_NEXT_HEADER = "modal-header",
MODAL_NEXT_CONTENT = "modal-content",
MODAL_NEXT = "modal",
MODAL_NEXT_OVERLAY = "modal-overlay",
FORMATTED_NUMBER = "formatted-number",
HIDDEN_TEXT = "hidden-text",
DIALOG_CONTENT_CONTAINER = "dialog-content-container",
Expand Down

0 comments on commit 2e0a86d

Please sign in to comment.