Files
element-call/src/AppBar.tsx
T

214 lines
6.3 KiB
TypeScript
Raw Normal View History

2025-06-26 05:08:57 -04:00
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
use,
useCallback,
useEffect,
useMemo,
useState,
createContext,
type FC,
type MouseEvent,
type ReactNode,
2025-06-26 05:08:57 -04:00
} from "react";
import classNames from "classnames";
import { Heading, IconButton, Text, Tooltip } from "@vector-im/compound-web";
2026-06-15 14:42:45 +02:00
import {
ArrowLeftIcon,
ChevronLeftIcon,
CollapseIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
2025-06-26 05:08:57 -04:00
import { useTranslation } from "react-i18next";
2026-04-10 18:45:34 +02:00
import { logger } from "matrix-js-sdk/lib/logger";
2025-06-26 05:08:57 -04:00
import { platform } from "./Platform";
import styles from "./AppBar.module.css";
interface AppBarContext {
setTitle: (value: string) => void;
setSubtitle: (value: ReactNode) => void;
2025-06-26 05:08:57 -04:00
setSecondaryButton: (value: ReactNode) => void;
2026-06-15 14:42:45 +02:00
setPrimaryButtonIconKind: (value: "back" | "minimise") => void;
2025-06-26 05:08:57 -04:00
setHidden: (value: boolean) => void;
}
const AppBarContext = createContext<AppBarContext | null>(null);
interface Props {
children: ReactNode;
}
/**
* A "top app bar" featuring a back button, title and possibly a secondary
* button, similar to what you might see in mobile apps.
*/
export const AppBar: FC<Props> = ({ children }) => {
const { t } = useTranslation();
const onBackClick = useCallback((e: MouseEvent) => {
e.preventDefault();
window.controls.onBackButtonPressed?.();
}, []);
const [title, setTitle] = useState<string>("");
const [subtitle, setSubtitle] = useState<ReactNode>(undefined);
2025-06-26 05:08:57 -04:00
const [hidden, setHidden] = useState<boolean>(false);
2026-04-10 18:46:44 +02:00
const [secondaryButton, setSecondaryButton] = useState<ReactNode | null>(
null,
);
2026-06-15 14:42:45 +02:00
const [primaryButtonIcon, setPrimaryButtonIconKind] = useState<
"back" | "minimise"
>("minimise");
2025-06-26 05:08:57 -04:00
const context = useMemo(
() => ({
setTitle,
setSubtitle,
setSecondaryButton,
setHidden,
2026-06-15 14:42:45 +02:00
setPrimaryButtonIconKind,
}),
[
setTitle,
setSubtitle,
setHidden,
setSecondaryButton,
setPrimaryButtonIconKind,
],
2025-06-26 05:08:57 -04:00
);
2026-06-15 14:42:45 +02:00
const BackIcon = platform === "android" ? ArrowLeftIcon : ChevronLeftIcon;
2025-06-26 05:08:57 -04:00
return (
<>
{/* Wrap the header in a div due to annoying z-index issues with the
gradient background */}
<div className={classNames(styles.bar, { [styles.hidden]: hidden })}>
<header>
<Tooltip label={t("common.back")}>
<IconButton
className={styles.primaryButton}
// We render the back button (PrimaryButtonIcon) the same size as the native os.
// We render the minimise icon (default) smaller as per designs.
size={primaryButtonIcon === "back" ? "32px" : "24px"}
onClick={onBackClick}
>
{primaryButtonIcon === "back" ? (
<BackIcon aria-hidden />
) : (
<CollapseIcon aria-hidden />
)}
</IconButton>
</Tooltip>
2025-06-26 05:08:57 -04:00
{title && (
<Heading
className={styles.title}
2025-06-26 05:08:57 -04:00
type="body"
size={platform === "ios" ? "md" : "lg"}
weight={platform === "ios" ? "semibold" : "medium"}
2025-06-26 05:08:57 -04:00
>
{title}
</Heading>
)}
{subtitle && (
<Text
className={styles.subtitle}
as="span"
size={platform === "ios" ? "sm" : "lg"}
>
{subtitle}
</Text>
)}
<div className={styles.secondaryButton}>{secondaryButton}</div>
</header>
2025-06-26 05:08:57 -04:00
</div>
<AppBarContext value={context}>{children}</AppBarContext>
</>
);
};
/**
* 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.
*/
export function useAppBarTitle(title: string): void {
const setTitle = use(AppBarContext)?.setTitle;
useEffect(() => {
if (setTitle !== undefined) {
setTitle(title);
return (): void => setTitle("");
}
}, [title, setTitle]);
}
/**
* React hook which sets the subtitle 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.
*/
export function useAppBarSubtitle(subtitle: ReactNode): void {
const setSubtitle = use(AppBarContext)?.setSubtitle;
useEffect(() => {
if (setSubtitle !== undefined) {
setSubtitle(subtitle);
return (): void => setSubtitle("");
}
}, [subtitle, setSubtitle]);
}
/**
2026-06-17 18:48:26 +02:00
* 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.
*/
2026-06-15 14:42:45 +02:00
export function useAppBarPrimaryButtonIconKind(
icon: "back" | "minimise",
): void {
const setIconKind = use(AppBarContext)?.setPrimaryButtonIconKind;
useEffect(() => {
2026-06-15 14:42:45 +02:00
if (setIconKind !== undefined) {
setIconKind(icon);
return (): void => setIconKind("minimise");
}
2026-06-15 14:42:45 +02:00
}, [setIconKind, icon]);
}
2025-06-26 05:08:57 -04:00
/**
* 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.
*/
export function useAppBarHidden(hidden: boolean): void {
const setHidden = use(AppBarContext)?.setHidden;
useEffect(() => {
if (setHidden !== undefined) {
setHidden(hidden);
return (): void => setHidden(false);
} else if (platform !== "desktop") {
logger.warn(
"[AppBar] useAppBarHidden called without AppBarContext provider, this will have no effect",
);
2025-06-26 05:08:57 -04:00
}
}, [setHidden, hidden]);
}
/**
* React hook which sets the secondary button 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.
*/
export function useAppBarSecondaryButton(button: ReactNode): void {
const setSecondaryButton = use(AppBarContext)?.setSecondaryButton;
useEffect(() => {
if (setSecondaryButton !== undefined) {
setSecondaryButton(button);
return (): void => setSecondaryButton("");
} else if (platform !== "desktop") {
2026-04-10 18:45:34 +02:00
logger.warn(
"[AppBar] useAppBarSecondaryButton called without AppBarContext provider, this will have no effect",
);
2025-06-26 05:08:57 -04:00
}
}, [button, setSecondaryButton]);
}