diff --git a/src/Banner.module.css b/src/QrCode.module.css
similarity index 78%
rename from src/Banner.module.css
rename to src/QrCode.module.css
index cd1b5272..500490bf 100644
--- a/src/Banner.module.css
+++ b/src/QrCode.module.css
@@ -1,5 +1,5 @@
/*
-Copyright 2023 New Vector Ltd
+Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.banner {
- flex: 1;
- border-radius: 8px;
- padding: 16px;
- background-color: var(--cpd-color-bg-subtle-primary);
+.qrCode img {
+ max-width: 100%;
+ image-rendering: pixelated;
+ border-radius: var(--cpd-space-4x);
}
diff --git a/src/Banner.tsx b/src/QrCode.test.tsx
similarity index 52%
rename from src/Banner.tsx
rename to src/QrCode.test.tsx
index 87ce8a96..6537076d 100644
--- a/src/Banner.tsx
+++ b/src/QrCode.test.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2023 New Vector Ltd
+Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,14 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { FC, ReactNode } from "react";
+import { describe, expect, test } from "vitest";
+import { render, configure } from "@testing-library/react";
-import styles from "./Banner.module.css";
+import { QrCode } from "./QrCode";
-interface Props {
- children: ReactNode;
-}
+configure({
+ defaultHidden: true,
+});
-export const Banner: FC
= ({ children }) => {
- return {children}
;
-};
+describe("QrCode", () => {
+ test("renders", async () => {
+ const { container, findByRole } = render(
+ ,
+ );
+ (await findByRole("img")) as HTMLImageElement;
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/src/QrCode.tsx b/src/QrCode.tsx
new file mode 100644
index 00000000..515234ab
--- /dev/null
+++ b/src/QrCode.tsx
@@ -0,0 +1,57 @@
+/*
+Copyright 2024 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { FC, useEffect, useState } from "react";
+import { toDataURL } from "qrcode";
+import classNames from "classnames";
+import { t } from "i18next";
+
+import styles from "./QrCode.module.css";
+
+interface Props {
+ data: string;
+ className?: string;
+}
+
+export const QrCode: FC = ({ data, className }) => {
+ const [url, setUrl] = useState(null);
+
+ useEffect(() => {
+ let isCancelled = false;
+
+ toDataURL(data, { errorCorrectionLevel: "L" })
+ .then((url) => {
+ if (!isCancelled) {
+ setUrl(url);
+ }
+ })
+ .catch((reason) => {
+ if (!isCancelled) {
+ setUrl(null);
+ }
+ });
+
+ return (): void => {
+ isCancelled = true;
+ };
+ }, [data]);
+
+ return (
+
+ {url &&

}
+
+ );
+};
diff --git a/src/Toast.test.tsx b/src/Toast.test.tsx
new file mode 100644
index 00000000..e2e2f9f7
--- /dev/null
+++ b/src/Toast.test.tsx
@@ -0,0 +1,85 @@
+/*
+Copyright 2023 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { describe, expect, test, vi } from "vitest";
+import { render, configure } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import { Toast } from "../src/Toast";
+import { withFakeTimers } from "./utils/test";
+
+configure({
+ defaultHidden: true,
+});
+
+// Test Explanation:
+// This test the toast. We need to use { document: window.document } because the toast listens
+// for user input on `window`.
+describe("Toast", () => {
+ test("renders", () => {
+ const { queryByRole } = render(
+ {}}>
+ Hello world!
+ ,
+ );
+ expect(queryByRole("dialog")).toBe(null);
+ const { getByRole } = render(
+ {}}>
+ Hello world!
+ ,
+ );
+ expect(getByRole("dialog")).toMatchSnapshot();
+ });
+
+ test("dismisses when Esc is pressed", async () => {
+ const user = userEvent.setup({ document: window.document });
+ const onDismiss = vi.fn();
+ render(
+
+ Hello world!
+ ,
+ );
+ await user.keyboard("[Escape]");
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ test("dismisses when background is clicked", async () => {
+ const user = userEvent.setup();
+ const onDismiss = vi.fn();
+ const { getByRole, unmount } = render(
+
+ Hello world!
+ ,
+ );
+ const background = getByRole("dialog").previousSibling! as Element;
+ await user.click(background);
+ expect(onDismiss).toHaveBeenCalled();
+ unmount();
+ });
+
+ test("dismisses itself after the specified timeout", () => {
+ withFakeTimers(() => {
+ const onDismiss = vi.fn();
+ render(
+
+ Hello world!
+ ,
+ );
+ vi.advanceTimersByTime(2000);
+ expect(onDismiss).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/Toast.tsx b/src/Toast.tsx
index fffbec97..e9c534f1 100644
--- a/src/Toast.tsx
+++ b/src/Toast.tsx
@@ -86,7 +86,7 @@ export const Toast: FC = ({
-
+
(
- (
- { state, className, children, ...rest }: TooltipProps,
- ref: ForwardedRef,
- ) => {
- const { tooltipProps } = useTooltip(rest, state);
-
- return (
-
- {children}
-
- );
- },
-);
-
-Tooltip.displayName = "Tooltip";
-
-interface TooltipTriggerProps {
- children: ReactElement;
- placement?: Placement;
- delay?: number;
- tooltip: () => string;
-}
-
-export const TooltipTrigger = forwardRef(
- (
- { children, placement, tooltip, ...rest }: TooltipTriggerProps,
- ref: ForwardedRef,
- ) => {
- const tooltipTriggerProps = { delay: 250, ...rest };
- const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
- const triggerRef = useObjectRef(ref);
- const overlayRef = useRef(null);
- const { triggerProps, tooltipProps } = useTooltipTrigger(
- tooltipTriggerProps,
- tooltipState,
- triggerRef,
- );
-
- const { overlayProps } = useOverlayPosition({
- placement: placement || "top",
- targetRef: triggerRef,
- overlayRef,
- isOpen: tooltipState.isOpen,
- offset: 12,
- });
-
- return (
-
- (
- children.props,
- rest,
- )}
- />
- {tooltipState.isOpen && (
-
-
- {tooltip()}
-
-
- )}
-
- );
- },
-);
-
-TooltipTrigger.displayName = "TooltipTrigger";
diff --git a/test/UrlParams-test.ts b/src/UrlParams.test.ts
similarity index 91%
rename from test/UrlParams-test.ts
rename to src/UrlParams.test.ts
index eb03d976..12076512 100644
--- a/test/UrlParams-test.ts
+++ b/src/UrlParams.test.ts
@@ -14,23 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { vi } from "vitest";
+import { describe, expect, it } from "vitest";
import { getRoomIdentifierFromUrl } from "../src/UrlParams";
-import { Config } from "../src/config/Config";
const ROOM_NAME = "roomNameHere";
const ROOM_ID = "!d45f138fsd";
const ORIGIN = "https://call.element.io";
-const HOMESERVER = "call.ems.host";
-
-vi.mock("../src/config/Config");
+const HOMESERVER = "localhost";
describe("UrlParams", () => {
- beforeAll(() => {
- vi.mocked(Config.defaultServerName).mockReturnValue("call.ems.host");
- });
-
describe("handles URL with /room/", () => {
it("and nothing else", () => {
expect(
diff --git a/src/UserMenu.module.css b/src/UserMenu.module.css
index 575b71b9..d4e06037 100644
--- a/src/UserMenu.module.css
+++ b/src/UserMenu.module.css
@@ -21,6 +21,14 @@ limitations under the License.
flex-shrink: 0;
}
+.userButton {
+ appearance: none;
+ background: none;
+ border: none;
+ margin: 0;
+ cursor: pointer;
+}
+
.userButton svg * {
fill: var(--cpd-color-icon-primary);
}
diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx
index 7cab4575..55ccc2c0 100644
--- a/src/UserMenu.tsx
+++ b/src/UserMenu.tsx
@@ -14,21 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { FC, ReactNode, useCallback, useMemo } from "react";
-import { Item } from "@react-stately/collections";
+import { FC, useMemo, useState } from "react";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
+import { Menu, MenuItem } from "@vector-im/compound-web";
-import { Button, LinkButton } from "./button";
-import { PopoverMenuTrigger } from "./popover/PopoverMenu";
-import { Menu } from "./Menu";
-import { TooltipTrigger } from "./Tooltip";
+import { LinkButton } from "./button";
import { Avatar, Size } from "./Avatar";
import UserIcon from "./icons/User.svg?react";
import SettingsIcon from "./icons/Settings.svg?react";
import LoginIcon from "./icons/Login.svg?react";
import LogoutIcon from "./icons/Logout.svg?react";
-import { Body } from "./typography/Typography";
import styles from "./UserMenu.module.css";
interface Props {
@@ -91,7 +87,7 @@ export const UserMenu: FC = ({
return arr;
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation, t]);
- const tooltip = useCallback(() => t("common.profile"), [t]);
+ const [open, setOpen] = useState(false);
if (!isAuthenticated) {
return (
@@ -102,10 +98,15 @@ export const UserMenu: FC = ({
}
return (
-
-
-
-
- {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (props: any): ReactNode => (
-
- )
+
}
-
+ >
+ {items.map(({ key, icon: Icon, label, dataTestid }) => (
+