diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx
new file mode 100644
index 00000000..3b894bde
--- /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 { afterAll, expect, test } from "vitest";
+import { render } from "@testing-library/react";
+import { act, ReactNode, useState } from "react";
+
+import { Modal } from "./Modal";
+
+test("that nothing is rendered when the modal is closed", () => {
+ const { queryByRole } = render(
+
+ This is the content.
+ ,
+ );
+ expect(queryByRole("dialog")).to.be.null();
+});
+
+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", () => {
+ function ModalFn(): ReactNode {
+ const [isOpen, setOpen] = useState(true);
+ return (
+ setOpen(false)}>
+ This is the content.
+
+ );
+ }
+ const { queryByRole, getByLabelText } = render();
+ act(() => {
+ getByLabelText("action.close").click();
+ });
+ expect(queryByRole("dialog")).to.be.null();
+});
+
+const originalMatchMedia = window.matchMedia;
+
+afterAll(() => {
+ window.matchMedia = originalMatchMedia;
+});
+
+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 deef7635..195b6592 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -91,6 +91,7 @@ export const Modal: FC = ({
)}
// Suppress the warning about there being no description; the modal
// has an accessible title
+ role="dialog"
aria-describedby={undefined}
{...rest}
>
@@ -116,7 +117,12 @@ 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 b98eb349..14d8cf03 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(