Add additional configuration options to Banner module (#34384)

* Add additional configuration options to Banner module

* Iterate

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-28 14:18:50 +00:00
committed by GitHub
parent 3d8a80271e
commit dfa6a58073
9 changed files with 94 additions and 25 deletions
+2 -1
View File
@@ -10,4 +10,5 @@ Please see LICENSE files in the repository root for full details.
@import url("maplibre-gl/dist/maplibre-gl.css") layer(app-web);
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound-tokens);
@import url("@vector-im/compound-web/dist/style.css") layer(compound-web);
@import url("@element-hq/web-shared-components/dist/element-web-shared-components.css") layer(shared-components);
/** Shared components is layer-aware and will fit itself into the correct layer */
@import url("@element-hq/web-shared-components/dist/element-web-shared-components.css");
+12 -5
View File
@@ -23,11 +23,10 @@ The `Menu` type is fulfilled by the following discriminated union:
### Static menu
| Key | Type | Description |
| ---------- | ---------------- | ------------------------------------------------------------------------- |
| type | "static" | The type for this menu config |
| logo_url | string, optional | URL to the logo to render in the menu, defaults to banner logo if omitted |
| categories | `[]Category` | Categories to render in the menu |
| Key | Type | Description |
| ---------- | ------------ | -------------------------------- |
| type | "static" | The type for this menu config |
| categories | `[]Category` | Categories to render in the menu |
The `Category` type is fulfilled by the following interface:
@@ -45,6 +44,14 @@ The `Link` type is fulfilled by the following interface:
| link_url | string | The URL to link to |
| target | string, optional | The `target` to use for this link |
### All menus additionally support the following optional keys:
| Key | Type | Description |
| ----------- | ---------------- | ------------------------------------------------------------------------- |
| logo_url | string, optional | URL to the logo to render in the menu, defaults to banner logo if omitted |
| logo_href | string, optional | URL to send the user to when clicking the logo in the menu |
| logo_height | number, optional | Height of the logo in pixels, defaults to 32 if omitted |
## Copyright & License
Copyright (c) 2025 New Vector Ltd
+42
View File
@@ -234,4 +234,46 @@ test.describe("Banner", () => {
await expect(axe).toHaveNoViolations();
});
});
test.describe("static config", () => {
test.use({
config: {
"io.element.element-web-modules.banner": {
logo_url: "https://domain/logo1.png",
logo_link_url: "https://domain",
title: "Title",
menu: {
type: "static",
logo_url: "https://domain/logo2.png",
logo_href: "https://domain/menu-link",
categories: [
{
name: "Category 1",
links: [
{
icon_uri: "https://domain/app1/logo.png",
name: "App 1",
link_url: "https://domain/app1",
},
],
},
],
},
},
},
});
test("should show a href on the menu logo if one is specified", async ({ page }) => {
await expect(page.getByLabel("Show portal")).toHaveAttribute("href", "https://domain");
const trigger = page.getByLabel("Show menu");
await trigger.click();
const sidebar = page.getByRole("dialog");
await expect(sidebar.getByRole("link", { name: "Show portal" })).toHaveAttribute(
"href",
"https://domain/menu-link",
);
});
});
});
+9 -4
View File
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type FC, type JSX, useState } from "react";
import { type ComponentProps, type FC, type JSX, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import * as Dialog from "@radix-ui/react-dialog";
import styled, { useTheme } from "styled-components";
@@ -179,7 +179,10 @@ const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
const [open, setOpen] = useState(false);
let content: JSX.Element;
let logoUrl = fallbackLogoUrl;
const logoProps: Omit<ComponentProps<typeof Logo>, "api"> = {
src: fallbackLogoUrl,
};
if (config instanceof Error) {
content = <CentredContainer>{api.i18n.translate("univention_error")}</CentredContainer>;
} else if (config) {
@@ -191,8 +194,10 @@ const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
</>
);
if (config.logo_url) {
logoUrl = config.logo_url;
logoProps.src = config.logo_url;
}
logoProps.height = config.logo_height !== undefined ? `${config.logo_height}px` : undefined;
logoProps.href = config.logo_href;
} else {
content = (
<CentredContainer>
@@ -231,7 +236,7 @@ const Menu: FC<Props> = ({ api, config, fallbackLogoUrl }) => {
>
<Dialog.Title>
<SidebarHeading>
<Logo api={api} src={logoUrl} />
<Logo {...logoProps} api={api} />
<Dialog.Close asChild>
<CloseButton
aria-label={api.i18n.translate("close_label")}
+17 -10
View File
@@ -11,15 +11,28 @@ import { Theme } from "./theme.ts";
z.config(z.locales.en());
const StaticConfig = z.object({
type: z.literal("static"),
const MenuConfig = z.object({
/**
* Alternative logo URL to display in the popover menu.
* Will use the main logo url if omitted.
*/
logo_url: z.optional(z.url()),
/**
* Height in pixels to use when rendering the logo in the menu, defaults to 32px.
* The width will be set automatically to maintain the original aspect ratio.
*/
logo_height: z.optional(z.number()),
/**
* Optional link href for the logo in the menu.
*/
logo_href: z.optional(z.url()),
});
const StaticConfig = z.extend(MenuConfig, {
type: z.literal("static"),
/**
* Categories of links to display in the menu.
*/
@@ -58,15 +71,9 @@ const StaticConfig = z.object({
export type StaticConfig = z.infer<typeof StaticConfig>;
const UniventionConfig = z.object({
const UniventionConfig = z.extend(MenuConfig, {
type: z.literal("univention"),
/**
* Alternative logo URL to display in the popover menu.
* Will use the main logo url if omitted.
*/
logo_url: z.optional(z.url()),
/**
* Base URL to an Intercom Service
* https://docs.software-univention.de/intercom-service/latest/architecture.html#endpoints
-2
View File
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
*/
import { ThemeProvider } from "styled-components";
import compound from "@vector-im/compound-web/dist/style.css" with { type: "css" };
import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
import Translations from "./translations.json";
@@ -29,7 +28,6 @@ class BannerModule implements Module {
return;
}
document.adoptedStyleSheets.push(compound);
document.adoptedStyleSheets.push(style);
this.api.i18n.register(Translations);
+2
View File
@@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
@import "@vector-im/compound-web/dist/style.css" layer(compound-layer);
/* Styles to ensure the banner does not push the app out of the viewport */
body {
display: flex;
+2 -3
View File
@@ -5,14 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import compound from "@vector-im/compound-web/dist/style.css" with { type: "css" };
import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
import Translations from "./translations.json";
import { ModuleConfig, CONFIG_KEY } from "./config";
import { name as ModuleName } from "../package.json";
import RoomPreviewBar from "./RoomPreviewBar.tsx";
import AuthFooter from "./AuthFooter.tsx";
import style from "./style.css" with { type: "css" };
const GUEST_INVISIBLE_COMPONENTS = [
"UIComponent.sendInvites",
@@ -37,7 +36,7 @@ class RestrictedGuestsModule implements Module {
return;
}
document.adoptedStyleSheets.push(compound);
document.adoptedStyleSheets.push(style);
this.api.i18n.register(Translations);
+8
View File
@@ -0,0 +1,8 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
@import "@vector-im/compound-web/dist/style.css" layer(compound-layer);