diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx
new file mode 100644
index 000000000..41bd7bbec
--- /dev/null
+++ b/src/Modal.test.tsx
@@ -0,0 +1,73 @@
+/*
+Copyright 2024 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only
+Please see LICENSE in the repository root for full details.
+*/
+
+import { expect, test } from "vitest";
+import { render } from "@testing-library/react";
+import { ReactNode, useState } from "react";
+import { afterEach } from "node:test";
+import userEvent from "@testing-library/user-event";
+
+import { Modal } from "./Modal";
+
+const originalMatchMedia = window.matchMedia;
+afterEach(() => {
+ window.matchMedia = originalMatchMedia;
+});
+
+test("that nothing is rendered when the modal is closed", () => {
+ const { queryByRole } = render(
+
+ This is the content.
+ ,
+ );
+ expect(queryByRole("dialog")).toBeNull();
+});
+
+test("the content is rendered when the modal is open", () => {
+ const { queryByRole } = render(
+
+ This is the content.
+ ,
+ );
+ expect(queryByRole("dialog")).toMatchSnapshot();
+});
+
+test("the modal can be closed by clicking the close button", async () => {
+ function ModalFn(): ReactNode {
+ const [isOpen, setOpen] = useState(true);
+ return (
+ setOpen(false)}>
+ This is the content.
+
+ );
+ }
+ const user = userEvent.setup();
+ const { queryByRole, getByRole } = render();
+ await user.click(getByRole("button", { name: "action.close" }));
+ expect(queryByRole("dialog")).toBeNull();
+});
+
+test("the modal renders as a drawer in mobile viewports", () => {
+ window.matchMedia = function (query): MediaQueryList {
+ return {
+ matches: query.includes("hover: none"),
+ addEventListener(): MediaQueryList {
+ return this as MediaQueryList;
+ },
+ removeEventListener(): MediaQueryList {
+ return this as MediaQueryList;
+ },
+ } as unknown as MediaQueryList;
+ };
+
+ const { queryByRole } = render(
+
+ This is the content.
+ ,
+ );
+ expect(queryByRole("dialog")).toMatchSnapshot();
+});
diff --git a/src/Modal.tsx b/src/Modal.tsx
index deef76355..6e9de90bb 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -89,6 +89,7 @@ export const Modal: FC = ({
styles.drawer,
{ [styles.tabbed]: tabbed },
)}
+ role="dialog"
// Suppress the warning about there being no description; the modal
// has an accessible title
aria-describedby={undefined}
@@ -114,9 +115,14 @@ export const Modal: FC = ({
- {/* Suppress the warning about there being no description; the modal
- has an accessible title */}
-
+
+
+
+
+
+ This is the content.
+
+
+
+
+`;
+
+exports[`the modal renders as a drawer in mobile viewports 1`] = `
+
+
+
+
+
+ This is the content.
+
+
+
+
+`;
diff --git a/src/useMediaQuery.ts b/src/useMediaQuery.ts
index b98eb3492..14d8cf03a 100644
--- a/src/useMediaQuery.ts
+++ b/src/useMediaQuery.ts
@@ -13,7 +13,7 @@ import { useEventTarget } from "./useEvents";
* React hook that tracks whether the given media query matches.
*/
export function useMediaQuery(query: string): boolean {
- const mediaQuery = useMemo(() => matchMedia(query), [query]);
+ const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
const [numChanges, setNumChanges] = useState(0);
useEventTarget(