diff --git a/src/AppBar.tsx b/src/AppBar.tsx index 9939f9505..490acf065 100644 --- a/src/AppBar.tsx +++ b/src/AppBar.tsx @@ -6,18 +6,22 @@ Please see LICENSE in the repository root for full details. */ import { - createContext, - type FC, - type MouseEvent, - type ReactNode, use, useCallback, useEffect, useMemo, useState, + createContext, + type FC, + type MouseEvent, + type ReactNode, } from "react"; import { Heading, IconButton, Tooltip } from "@vector-im/compound-web"; -import { CollapseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + ArrowLeftIcon, + ChevronLeftIcon, + CollapseIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { useTranslation } from "react-i18next"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -28,6 +32,7 @@ import styles from "./AppBar.module.css"; interface AppBarContext { setTitle: (value: string) => void; setSecondaryButton: (value: ReactNode) => void; + setPrimaryButtonIconKind: (value: "back" | "minimise") => void; setHidden: (value: boolean) => void; } @@ -53,11 +58,22 @@ export const AppBar: FC = ({ children }) => { const [secondaryButton, setSecondaryButton] = useState( null, ); + const [primaryButtonIcon, setPrimaryButtonIconKind] = useState< + "back" | "minimise" + >("minimise"); + const context = useMemo( - () => ({ setTitle, setSecondaryButton, setHidden }), - [setTitle, setHidden, setSecondaryButton], + () => ({ + setTitle, + setSecondaryButton, + setHidden, + setPrimaryButtonIconKind, + }), + [setTitle, setHidden, setSecondaryButton, setPrimaryButtonIconKind], ); + const BackIcon = platform === "android" ? ArrowLeftIcon : ChevronLeftIcon; + return ( <>
= ({ children }) => { > - - + + {primaryButtonIcon === "back" ? ( + + ) : ( + + )} @@ -107,6 +132,22 @@ export function useAppBarTitle(title: string): void { }, [title, setTitle]); } +/** + * React hook which sets the primary button icon kind. Can only be "minimise" or "back" + * It is an error to call this hook from multiple sites in the same component tree. + */ +export function useAppBarPrimaryButtonIconKind( + icon: "back" | "minimise", +): void { + const setIconKind = use(AppBarContext)?.setPrimaryButtonIconKind; + useEffect(() => { + if (setIconKind !== undefined) { + setIconKind(icon); + return (): void => setIconKind("minimise"); + } + }, [setIconKind, icon]); +} + /** * React hook which sets the title to be shown in the app bar, if present. It is * an error to call this hook from multiple sites in the same component tree. diff --git a/src/room/LobbyView.test.tsx b/src/room/LobbyView.test.tsx index 487501af7..4131529cd 100644 --- a/src/room/LobbyView.test.tsx +++ b/src/room/LobbyView.test.tsx @@ -11,6 +11,10 @@ import { BrowserRouter } from "react-router-dom"; import { TooltipProvider } from "@vector-im/compound-web"; import { type MatrixClient } from "matrix-js-sdk"; import { axe } from "vitest-axe"; +import { + ArrowLeftIcon, + ChevronLeftIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { LobbyView } from "./LobbyView"; import { E2eeType } from "../e2ee/e2eeType"; @@ -20,6 +24,7 @@ import { type ProcessorState } from "../livekit/TrackProcessorContext"; import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import lobbyStyles from "./LobbyView.module.css"; import headerStyles from "../Header.module.css"; +import { AppBar } from "../AppBar"; vi.mock("@livekit/components-react", () => ({ usePreviewTracks: (): unknown[] => [], @@ -47,6 +52,13 @@ const mockClient = { getDeviceId: () => "DEVICE", } as Partial as MatrixClient; +const platformMock = vi.hoisted(() => vi.fn(() => "desktop")); +vi.mock("../Platform", () => ({ + get platform(): string { + return platformMock(); + }, +})); + const matrixInfo = { userId: "@user:example.org", displayName: "Test User", @@ -60,25 +72,32 @@ const matrixInfo = { function renderLobbyView( props: Partial[0]> = {}, + withAppBar = false, + platform = "android", ): ReturnType { + platformMock.mockReturnValue(platform); const mediaDevices = mockMediaDevices({}); const muteStates = mockMuteStates(); - + const hideHeader = withAppBar ? true : false; + const lobbyView = ( + {}} + confineToRoom={false} + hideHeader={hideHeader} + participantCount={3} + onShareClick={null} + {...props} + /> + ); return render( - {}} - confineToRoom={false} - hideHeader={false} - participantCount={3} - onShareClick={null} - {...props} - /> + {withAppBar && {lobbyView}} + {!withAppBar && lobbyView} , @@ -97,9 +116,10 @@ describe("LobbyView", () => { it("renders without header", () => { const { container } = renderLobbyView({ hideHeader: true }); - expect( - container.getElementsByClassName(headerStyles.header).length, - ).toBeFalsy(); + const els = container.getElementsByClassName(headerStyles.header); + for (const el of els) { + expect(el).not.toBeVisible(); + } }); it("renders with waiting for invite state", () => { @@ -108,4 +128,56 @@ describe("LobbyView", () => { }); expect(getByTestId("lobby_joinCall")).toHaveClass(lobbyStyles.wait); }); + + it("renders with AppBar android", async () => { + const { container } = renderLobbyView( + { + waitingForInvite: true, + }, + true, + "android", + ); + expect( + container.getElementsByClassName(headerStyles.header).length, + ).toBeTruthy(); + // Check that the primary button uses ArrowLeftIcon (the back/return icon), + // not the default CollapseIcon + const { container: iconContainer } = render(); + const expectedSvgPath = iconContainer + .querySelector("path")! + .getAttribute("d"); + const primaryButtonSvgPath = container + .querySelector(".leftNav button") + ?.querySelector("path") + ?.getAttribute("d"); + expect(primaryButtonSvgPath).toBe(expectedSvgPath); + expect(container).toMatchSnapshot(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it("renders with AppBar ios", async () => { + const { container } = renderLobbyView( + { + waitingForInvite: true, + }, + true, + "ios", + ); + expect( + container.getElementsByClassName(headerStyles.header).length, + ).toBeTruthy(); + // Check that the primary button uses ArrowLeftIcon (the back/return icon), + // not the default CollapseIcon + const { container: iconContainer } = render(); + const expectedSvgPath = iconContainer + .querySelector("path")! + .getAttribute("d"); + const primaryButtonSvgPath = container + .querySelector(".leftNav button") + ?.querySelector("path") + ?.getAttribute("d"); + expect(primaryButtonSvgPath).toBe(expectedSvgPath); + expect(container).toMatchSnapshot(); + expect(await axe(container)).toHaveNoViolations(); + }); }); diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index cec9f6acd..e122b3f26 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -51,6 +51,7 @@ import { CallFooter, type FooterSnapshot } from "../components/CallFooter"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { createLobbyFooterViewModel } from "../components/CallFooterViewModel"; import { type ViewModel } from "../state/ViewModel"; +import { useAppBarPrimaryButtonIconKind } from "../AppBar"; interface Props { client: MatrixClient; @@ -85,8 +86,9 @@ export const LobbyView: FC = ({ }, []); const { t } = useTranslation(); - usePageTitle(matrixInfo.roomName); + usePageTitle(matrixInfo.roomName); + useAppBarPrimaryButtonIconKind("back"); const audioEnabled = useBehavior(muteStates.audio.enabled$); const videoEnabled = useBehavior(muteStates.video.enabled$); const toggleAudio = useBehavior(muteStates.audio.toggle$); diff --git a/src/room/__snapshots__/LobbyView.test.tsx.snap b/src/room/__snapshots__/LobbyView.test.tsx.snap index bd6d6ed16..e19c3477f 100644 --- a/src/room/__snapshots__/LobbyView.test.tsx.snap +++ b/src/room/__snapshots__/LobbyView.test.tsx.snap @@ -1,5 +1,481 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`LobbyView > renders with AppBar android 1`] = ` +
+
+
+ +
+
+
+
+
+
+ + Back to recents + +
+ +
+
+`; + +exports[`LobbyView > renders with AppBar ios 1`] = ` +
+
+
+ +
+
+
+
+
+
+ + Back to recents + +
+ +
+
+`; + exports[`LobbyView > renders with header and participant count 1`] = `
renders with header and participant count 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -299,7 +775,7 @@ exports[`LobbyView > renders with header and participant count 1`] = ` xmlns="http://www.w3.org/2000/svg" >