diff --git a/docs/component-catalog.json b/docs/component-catalog.json index bf869195a6..6faf6f8934 100644 --- a/docs/component-catalog.json +++ b/docs/component-catalog.json @@ -478,7 +478,7 @@ { "name": "ModalV2", "importPath": "@lifesg/react-design-system/modal-v2", - "description": "A composable modal dialog with slot-based content layout. Use `ModalV2` when you need a controlled overlay dialog with structured content slots. Sub-components: - `ModalV2.Card` — wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports a `fullscreen` prop that makes the card fill the viewport. - `ModalV2.CloseButton` — renders an accessible close button that invokes the modal's `onClose` callback. - `ModalV2.Content` — container for the body content of the modal card. - `ModalV2.Footer` — footer area for primary and secondary action buttons.", + "description": "A composable modal dialog with slot-based content layout. Use `ModalV2` when you need a controlled overlay dialog with structured content slots. Sub-components: - `ModalV2.Card` — wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports a `fullscreen` prop that makes the card fill the viewport, and a `fillHeight` prop that stretches the slots to fill a size-constrained card. - `ModalV2.CloseButton` — renders an accessible close button that invokes the modal's `onClose` callback. - `ModalV2.Content` — container for the body content of the modal card. - `ModalV2.Footer` — footer area for primary and secondary action buttons.", "keywords": ["dialog", "overlay", "popup"] }, { diff --git a/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile---modal-open.png b/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile---modal-open.png index 6f4a28068e..273ebf9249 100644 Binary files a/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile---modal-open.png and b/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile---modal-open.png differ diff --git a/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile-landscape---modal-open.png b/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile-landscape---modal-open.png index cb214a9615..20c7c90a00 100644 Binary files a/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile-landscape---modal-open.png and b/e2e/tests/components/e-signature/__screenshots__/chromium/ESignature-Modal-mobile-landscape---modal-open.png differ diff --git a/src/e-signature/e-signature.styles.ts b/src/e-signature/e-signature.styles.ts index ad12de9a56..7c71d83ca2 100644 --- a/src/e-signature/e-signature.styles.ts +++ b/src/e-signature/e-signature.styles.ts @@ -1,11 +1,7 @@ import { css } from "@linaria/core"; -import * as modalStyles from "../modal/modal.styles"; -import { Border, Colour, Font, MediaQuery, Radius, Spacing } from "../theme"; - -// ============================================================================= -// STYLING -// ============================================================================= +import { headerTokens, slotSpacerTokens } from "../modal-v2/slots/slot-styles"; +import { Border, Colour, MediaQuery, Radius, Shadow, Spacing } from "../theme"; // ----------------------------------------------------------------------------- // MAIN FIELD @@ -71,56 +67,69 @@ export const instructions = css` // SIGNATURE MODAL // ----------------------------------------------------------------------------- -export const scrollableModal = css` - /* increase specificity as the styles are overwritten */ - && { - height: 100%; - overflow-y: auto; - } -`; - -export const growContainer = css` - width: 100%; - margin: auto; - padding: ${Spacing["layout-xxl"]} ${Spacing["layout-sm"]}; - - ${MediaQuery.MaxWidth.sm}, &[data-mobile-landscape="true"] { - padding: 0; - width: 100%; - height: 100%; - } -`; - -export const modalBox = css` +export const modalCard = css` width: 100%; height: 29rem; max-width: 672px; max-height: none; margin: 0 auto; padding: ${Spacing["spacing-16"]}; + box-shadow: ${Shadow["lg-strong"]}; - ${MediaQuery.MaxWidth.sm}, &[data-mobile-landscape="true"] { - display: flex; - flex-direction: column; - max-width: none; - height: 100%; - border-radius: 0; - padding: 0; + ${slotSpacerTokens.contentLastChildMarginBottom}: 0; +`; + +export const modalHeader = css` + ${headerTokens.padding}: 0 0 ${Spacing["spacing-16"]}; + position: relative; + + & > button { + position: absolute; + top: 0; + right: 0; + } - ${modalStyles.tokens.closeButton.topInset}: ${Spacing["spacing-8"]}; - ${modalStyles.tokens.closeButton.rightInset}: ${Spacing["spacing-20"]}; + & > div[aria-hidden] { + display: none; + } + + ${MediaQuery.MaxWidth.sm} { + &[data-mobile-landscape] { + ${headerTokens.padding}: ${Spacing["spacing-12"]} + ${Spacing["spacing-20"]}; + } + + &[data-mobile-landscape] > button { + top: ${Spacing["spacing-8"]}; + right: ${Spacing["spacing-20"]}; + } + } + + &[data-mobile-landscape="true"] { + ${headerTokens.padding}: ${Spacing["spacing-12"]} + ${Spacing["spacing-20"]}; + } + + &[data-mobile-landscape="true"] > button { + top: ${Spacing["spacing-8"]}; + right: ${Spacing["spacing-20"]}; } `; -export const modalTitle = css` - ${Font["body-baseline-semibold"]} - color: ${Colour["text"]}; - margin-bottom: ${Spacing["spacing-16"]}; - text-align: center; +export const modalContent = css` + margin: 0; + display: flex; + flex-direction: column; + min-height: 0; - ${MediaQuery.MaxWidth.sm}, &[data-mobile-landscape="true"] { - ${Font["body-md-semibold"]} - margin: ${Spacing["spacing-12"]} 0; + ${MediaQuery.MaxWidth.sm} { + &[data-mobile-landscape] { + flex: 1; + } + } + + &[data-mobile-landscape="true"] { + flex: 1; } `; @@ -130,12 +139,16 @@ export const eSignatureContainer = css` border-radius: ${Radius["lg"]}; overflow: hidden; - ${MediaQuery.MaxWidth.sm}, &[data-mobile-landscape="true"] { - border-radius: 0; - flex: 1; + ${MediaQuery.MaxWidth.sm} { + &[data-mobile-landscape] { + border-radius: 0; + flex: 1; + } } &[data-mobile-landscape="true"] { + height: auto; + flex: 1; background: ${Colour["bg-strong"]}; } `; @@ -184,28 +197,26 @@ export const modalButtons = css` justify-content: space-between; margin-top: ${Spacing["spacing-16"]}; - ${MediaQuery.MaxWidth.sm} { - flex-direction: column-reverse; - margin: ${Spacing["spacing-16"]} ${Spacing["spacing-24"]} - ${Spacing["spacing-48"]}; - gap: ${Spacing["spacing-16"]}; + & > button { + width: 8.5rem; } - &[data-mobile-landscape="true"] { - flex-direction: row; - margin: ${Spacing["spacing-16"]} ${Spacing["spacing-20"]}; - } -`; - -export const modalActionButton = css` - width: 8.5rem; - ${MediaQuery.MaxWidth.sm} { - width: 100%; + &[data-mobile-landscape] { + flex-direction: column-reverse; + margin: ${Spacing["spacing-16"]} ${Spacing["spacing-24"]} + ${Spacing["spacing-48"]}; + gap: ${Spacing["spacing-16"]}; + } + + &[data-mobile-landscape] > button { + width: 100%; + } } &[data-mobile-landscape="true"] { - height: 2.5rem; + flex-direction: row; + margin: ${Spacing["spacing-16"]} ${Spacing["spacing-20"]}; } `; diff --git a/src/e-signature/e-signature.tsx b/src/e-signature/e-signature.tsx index b56d620d33..2451e43013 100644 --- a/src/e-signature/e-signature.tsx +++ b/src/e-signature/e-signature.tsx @@ -3,7 +3,7 @@ import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { Button } from "../button"; import { DashedBorder } from "../dashed-border"; -import { Modal } from "../modal"; +import { ModalV2 } from "../modal-v2"; import { ProgressBar } from "../shared/progress-bar"; import { Border, @@ -142,26 +142,37 @@ export const ESignature = (props: EsignatureProps) => { const renderModal = () => { return ( - setShowModal(false)} > -
- + Signature + + ) : ( + + Signature + + ) + } + closeButtonPosition="right" + className={styles.modalHeader} + data-mobile-landscape={isMobileLandscape} + /> + setShowModal(false)} > -

- Signature -

{ data-mobile-landscape={isMobileLandscape} >
-
-
-
+ + + ); }; diff --git a/src/modal-v2/modal-v2.tsx b/src/modal-v2/modal-v2.tsx index d2a4dc9319..e911a971d3 100644 --- a/src/modal-v2/modal-v2.tsx +++ b/src/modal-v2/modal-v2.tsx @@ -138,14 +138,14 @@ const Base = ({ * content slots. * * Sub-components: - * - `ModalV2.Card` — wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports a `fullscreen` prop that makes the card fill the viewport. + * - `ModalV2.Card` — wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports a `fullscreen` prop that makes the card fill the viewport, and a `fillHeight` prop that stretches the slots to fill a size-constrained card. * - `ModalV2.CloseButton` — renders an accessible close button that invokes the modal's `onClose` callback. * - `ModalV2.Content` — container for the body content of the modal card. * - `ModalV2.Footer` — footer area for primary and secondary action buttons. * @keywords dialog, overlay, popup */ export const ModalV2 = Object.assign(Base, { - /** Wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports a `fullscreen` prop. */ + /** Wraps modal content and arranges `Content`, `Footer`, and `CloseButton` slots in the correct layout. Supports `fullscreen` and `fillHeight` props. */ Card, /** Renders an accessible close button that invokes the modal's `onClose` callback. */ CloseButton, diff --git a/src/modal-v2/slots/card.tsx b/src/modal-v2/slots/card.tsx index 22e43ac5fd..8384b45cc8 100644 --- a/src/modal-v2/slots/card.tsx +++ b/src/modal-v2/slots/card.tsx @@ -19,6 +19,7 @@ function CardInner( className, elementRef, fullscreen, + fillHeight, ...otherProps }: ModalCardProps, ref: React.ForwardedRef @@ -62,6 +63,7 @@ function CardInner( ref={mergeRefs(ref, elementRef)} id={id} data-testid={testId} + data-fullscreen={fullscreen || undefined} {...otherProps} onClick={handleOnClick} className={clsx( @@ -74,7 +76,7 @@ function CardInner( data-has-close-button={!!hasHeader} className={clsx( styles.slotSpacer, - fullscreen && styles.fullscreenSlotSpacer + (fullscreen || fillHeight) && styles.fillHeightSlotSpacer )} > {ContentSlot} diff --git a/src/modal-v2/slots/header.tsx b/src/modal-v2/slots/header.tsx index c6951fbfd1..21969a34c4 100644 --- a/src/modal-v2/slots/header.tsx +++ b/src/modal-v2/slots/header.tsx @@ -33,7 +33,7 @@ export const Header = ({ > - {title && {title}} + {title &&
{title}
} {title &&
}
); diff --git a/src/modal-v2/slots/slot-styles.ts b/src/modal-v2/slots/slot-styles.ts index 8f1da1ab70..0644dd0a63 100644 --- a/src/modal-v2/slots/slot-styles.ts +++ b/src/modal-v2/slots/slot-styles.ts @@ -34,62 +34,92 @@ export const slotSpacerTokens = { "--fds-internal-modalV2-slotSpacer-footerLastChildMarginBottom", } as const; +export const headerTokens = { + padding: "--fds-internal-modalV2-header-padding", +} as const; + // ============================================================================= // STYLING // ============================================================================= /** Shared by both the windowed (`modalCard`) and `fullscreenModalCard` variants. */ export const cardBase = css` - background: ${Colour.bg}; + :where(&) { + background: ${Colour.bg}; - display: flex; - flex-direction: column-reverse; + display: flex; + flex-direction: column-reverse; + } `; export const modalCard = css` - width: 40rem; - margin: ${Spacing["spacing-64"]} auto; - box-shadow: ${Shadow["xs-strong"]}; - border-radius: ${Radius["lg"]}; + :where(&) { + width: 40rem; + margin: ${Spacing["spacing-64"]} auto; + box-shadow: ${Shadow["xs-strong"]}; + border-radius: ${Radius["lg"]}; - max-width: calc(100% - ${Breakpoint["xxl-margin"]} * 2); + max-width: calc(100% - ${Breakpoint["xxl-margin"]} * 2); - ${MediaQuery.MaxWidth.xl} { - max-width: calc(100% - ${Breakpoint["xl-margin"]} * 2); - } + ${MediaQuery.MaxWidth.xl} { + max-width: calc(100% - ${Breakpoint["xl-margin"]} * 2); + } - ${MediaQuery.MaxWidth.lg} { - max-width: calc(100% - ${Breakpoint["lg-margin"]} * 2); - } + ${MediaQuery.MaxWidth.lg} { + max-width: calc(100% - ${Breakpoint["lg-margin"]} * 2); + } - ${MediaQuery.MaxWidth.md} { - max-width: calc(100% - ${Breakpoint["md-margin"]} * 2); - } + ${MediaQuery.MaxWidth.md} { + max-width: calc(100% - ${Breakpoint["md-margin"]} * 2); + } - ${MediaQuery.MaxWidth.sm} { - max-width: calc(100% - ${Breakpoint["sm-margin"]} * 2); - } + ${MediaQuery.MaxWidth.sm} { + max-width: calc(100% - ${Breakpoint["sm-margin"]} * 2); + } - ${MediaQuery.MaxWidth.xs} { - max-width: calc(100% - ${Breakpoint["xs-margin"]} * 2); - } + ${MediaQuery.MaxWidth.xs} { + max-width: calc(100% - ${Breakpoint["xs-margin"]} * 2); + } - ${MediaQuery.MaxWidth.xxs} { - max-width: calc(100% - ${Breakpoint["xxs-margin"]} * 2); + ${MediaQuery.MaxWidth.xxs} { + max-width: calc(100% - ${Breakpoint["xxs-margin"]} * 2); + } } `; +/* + * Deliberately high specificity, unlike the other card styles: `fullscreen` has + * to win over sizing and spacing that a consumer sets via `className` (e.g. + * e-signature's own modalCard pins height/max-width/padding/box-shadow). That + * is also why every property it needs to neutralise is reset explicitly here + * rather than relying on `modalCard` being absent. + */ export const fullscreenModalCard = css` - width: 100%; - height: 100vh; + &&[data-fullscreen] { + width: 100%; + height: 100vh; + max-width: none; + margin: 0; + padding: 0; + border-radius: 0; + box-shadow: none; + } `; export const closeButtonContainer = css` - margin-right: ${Spacing["spacing-16"]}; - margin-left: auto; - margin-top: ${Spacing["spacing-16"]}; - margin-bottom: ${Spacing["spacing-16"]}; + :where(&) { + margin-right: ${Spacing["spacing-16"]}; + margin-left: auto; + margin-top: ${Spacing["spacing-16"]}; + margin-bottom: ${Spacing["spacing-16"]}; + } `; +/* + * Applied to a ClickableIcon, so these declarations are deliberately NOT + * wrapped in :where(): they have to outrank ClickableIcon's own `main` class + * (which sets `padding: spacing-24` and sizes `svg` to 1.125rem). Dropping + * them to zero specificity lets those defaults win and inflates the card. + */ export const styledClickableIcon = css` padding: 0; color: ${Colour.icon}; @@ -101,86 +131,84 @@ export const styledClickableIcon = css` `; export const contentContainer = css` - margin-right: ${Spacing["spacing-64"]}; - margin-left: ${Spacing["spacing-64"]}; - - ${MediaQuery.MaxWidth.sm} { - margin-right: ${Spacing["spacing-20"]}; - margin-left: ${Spacing["spacing-20"]}; + :where(&) { + margin-right: ${Spacing["spacing-64"]}; + margin-left: ${Spacing["spacing-64"]}; + + ${MediaQuery.MaxWidth.sm} { + margin-right: ${Spacing["spacing-20"]}; + margin-left: ${Spacing["spacing-20"]}; + } } `; export const footerContainer = css` - margin-right: ${Spacing["spacing-64"]}; - margin-left: ${Spacing["spacing-64"]}; - - ${MediaQuery.MaxWidth.sm} { - margin-right: ${Spacing["spacing-20"]}; - margin-left: ${Spacing["spacing-20"]}; + :where(&) { + margin-right: ${Spacing["spacing-64"]}; + margin-left: ${Spacing["spacing-64"]}; + + ${MediaQuery.MaxWidth.sm} { + margin-right: ${Spacing["spacing-20"]}; + margin-left: ${Spacing["spacing-20"]}; + } + + display: flex; + flex-direction: row-reverse; /* primary button on right */ + column-gap: ${Spacing["spacing-32"]}; + row-gap: ${Spacing["spacing-16"]}; + + ${MediaQuery.MaxWidth.md} { + flex-direction: column; + } } - display: flex; - flex-direction: row-reverse; /* primary button on right */ - column-gap: ${Spacing["spacing-32"]}; - row-gap: ${Spacing["spacing-16"]}; - - & > button { + :where(& > button) { flex: 1; } - - ${MediaQuery.MaxWidth.md} { - flex-direction: column; - } `; export const slotSpacer = css` - ${slotSpacerTokens.contentLastChildMarginBottom}: initial; - ${slotSpacerTokens.footerNotFirstChildMarginTop}: initial; - ${slotSpacerTokens.footerLastChildMarginBottom}: initial; - ${slotSpacerTokens.firstChildMarginTopWithCloseButton}: initial; - ${slotSpacerTokens.firstChildMarginTopNoCloseButton}: initial; - - ${internalSlotSpacerTokens.firstChildMarginTop}: var( - ${slotSpacerTokens.firstChildMarginTopNoCloseButton}, - ${Spacing["spacing-64"]} - ); - - & > :where(.${contentContainer}:last-child) { + :where(&) { + ${internalSlotSpacerTokens.firstChildMarginTop}: var( + ${slotSpacerTokens.firstChildMarginTopNoCloseButton}, + ${Spacing["spacing-64"]} + ); + } + + :where(& > .${contentContainer}:last-child) { margin-bottom: var( ${slotSpacerTokens.contentLastChildMarginBottom}, ${Spacing["spacing-64"]} ); } - & > :where(.${footerContainer}:not(:first-child)) { + :where(& > .${footerContainer}:not(:first-child)) { margin-top: var( ${slotSpacerTokens.footerNotFirstChildMarginTop}, ${Spacing["spacing-32"]} ); } - & > :where(.${footerContainer}:last-child) { + :where(& > .${footerContainer}:last-child) { margin-bottom: var( ${slotSpacerTokens.footerLastChildMarginBottom}, ${Spacing["spacing-64"]} ); } - & - > :where( - .${contentContainer}:first-child, .${footerContainer}:first-child - ) { + :where(& > .${contentContainer}:first-child), + :where(& > .${footerContainer}:first-child) { margin-top: var(${internalSlotSpacerTokens.firstChildMarginTop}); } - &[${slotSpacerHasCloseButtonAttribute}="true"] { + :where(&[${slotSpacerHasCloseButtonAttribute}="true"]) { ${internalSlotSpacerTokens.firstChildMarginTop}: var( ${slotSpacerTokens.firstChildMarginTopWithCloseButton}, 0 ); } - &[${slotSpacerHasCloseButtonAttribute}="false"] { + :where(&[${slotSpacerHasCloseButtonAttribute}="false"]) { ${internalSlotSpacerTokens.firstChildMarginTop}: var( ${slotSpacerTokens.firstChildMarginTopNoCloseButton}, ${Spacing["spacing-64"]} @@ -188,31 +216,40 @@ export const slotSpacer = css` } `; -export const fullscreenSlotSpacer = css` - flex-grow: 1; +/** Lets the slot area absorb the leftover height of a size-constrained card. */ +export const fillHeightSlotSpacer = css` + :where(&) { + flex: 1; + min-height: 0; - display: flex; - flex-direction: column; - justify-content: space-between; + display: flex; + flex-direction: column; + justify-content: space-between; + } `; export const headerContainer = css` - display: flex; - align-items: center; - padding: ${Spacing["spacing-16"]}; + :where(&) { + display: flex; + align-items: center; + padding: var(${headerTokens.padding}, ${Spacing["spacing-16"]}); + } - &[data-close-button-position="right"] { + :where(&[data-close-button-position="right"]) { flex-direction: row-reverse; } `; export const headerTitle = css` - ${Font["heading-xs-semibold"]} - color: ${Colour.text}; - flex: 1; - text-align: center; + :where(&) { + ${Font["heading-xs-semibold"]} + color: ${Colour.text}; + flex: 1; + text-align: center; + } `; +/* Also a ClickableIcon — see the note on styledClickableIcon above. */ export const headerCloseButton = css` padding: 0; color: ${Colour.icon}; @@ -225,6 +262,8 @@ export const headerCloseButton = css` `; export const headerSpacer = css` - width: 2rem; - flex-shrink: 0; + :where(&) { + width: 2rem; + flex-shrink: 0; + } `; diff --git a/src/modal-v2/types.ts b/src/modal-v2/types.ts index af3ba85f11..18a346ff4d 100644 --- a/src/modal-v2/types.ts +++ b/src/modal-v2/types.ts @@ -22,6 +22,12 @@ export interface ModalCardProps extends React.HTMLAttributes { * padding, or box-shadow. */ fullscreen?: boolean | undefined; + /** + * When `true`, the slot area stretches to absorb any leftover height of a + * size-constrained card. Use this when the card is given an explicit height + * and its content should fill it. Implied by `fullscreen`. + */ + fillHeight?: boolean | undefined; } // @storybookSection ModalV2.CloseButton @@ -40,16 +46,18 @@ export interface ModalCloseButtonProps /** * Props for the `ModalV2.Header` slot. */ -export interface ModalHeaderProps extends React.HTMLAttributes { +export interface ModalHeaderProps + extends Omit, "title"> { /** * @default "modal-header" */ "data-testid"?: string | undefined; /** - * Heading text rendered in the header. Always centered regardless of + * Heading rendered in the header. Accepts a plain string, or a node when + * you need to control the typography. Always centered regardless of * `closeButtonPosition`. Omit for a close-button-only header. */ - title?: string | undefined; + title?: React.ReactNode | undefined; /** * Which side of the header the close button is placed on. The title * alignment adjusts automatically. diff --git a/tests/e-signature/e-signature.spec.tsx b/tests/e-signature/e-signature.spec.tsx index 6d8130eb92..ce22798e8c 100644 --- a/tests/e-signature/e-signature.spec.tsx +++ b/tests/e-signature/e-signature.spec.tsx @@ -1,4 +1,10 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import { ESignature } from "src/e-signature"; import { createMatchMediaMock } from "../_common"; @@ -85,6 +91,7 @@ describe("ESignature", () => { }); it("should call onChange and show signature preview on clicking save button", async () => { + jest.useFakeTimers(); const changeFn = jest.fn(); render(); @@ -92,13 +99,15 @@ describe("ESignature", () => { drawSignature(); fireEvent.click(screen.getByRole("button", { name: "Save" })); - await waitFor(() => { - expect(getSignatureModal()).not.toBeVisible(); + await act(async () => { + jest.advanceTimersByTime(500); }); + expect(queryAddSignatureButton()).not.toBeInTheDocument(); expect(getEditSignatureButton()).toBeInTheDocument(); expect(changeFn).toHaveBeenCalled(); expect(screen.getByAltText("Signature preview")).toBeInTheDocument(); + jest.useRealTimers(); }); it("should discard unsaved changes on clicking cross button in modal", () => { @@ -115,6 +124,7 @@ describe("ESignature", () => { }); it("should clear the field value on clicking clear button and save button subsequently", async () => { + jest.useFakeTimers(); render(); fireEvent.click(getAddSignatureButton()); @@ -122,10 +132,12 @@ describe("ESignature", () => { fireEvent.click(screen.getByRole("button", { name: "Clear" })); fireEvent.click(screen.getByRole("button", { name: "Save" })); - await waitFor(() => { - expect(getSignatureModal()).not.toBeVisible(); + await act(async () => { + jest.advanceTimersByTime(500); }); + expect(getAddSignatureButton()).toBeInTheDocument(); + jest.useRealTimers(); }); }); diff --git a/tests/modal-v2/modal-v2.spec.tsx b/tests/modal-v2/modal-v2.spec.tsx index 769916d379..e7cc93d3db 100644 --- a/tests/modal-v2/modal-v2.spec.tsx +++ b/tests/modal-v2/modal-v2.spec.tsx @@ -89,4 +89,21 @@ describe("ModalV2", () => { expect(mockClose).toHaveBeenCalled(); expect(mockOverlayClick).not.toHaveBeenCalled(); }); + + it("should render the Header title and trigger onClose from its close button", () => { + const mockClose = jest.fn(); + render( + + + + + + ); + + expect(screen.getByTestId("modal-header")).toBeInTheDocument(); + expect(screen.getByText("Signature")).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("close-button")); + expect(mockClose).toHaveBeenCalled(); + }); });