Refactor and redesign user menu (#32812)
* Initial quick settings menu * Total refactor * Quick design fixes. * Refactor to use a view model. * Remove unused strings * Apply label * Refactor naming * Fixup most tests * Remove specific theming for old user menu * prettier * Lots of cleanup * Allow overriding the menu classes * update snap * Oops translations * tidy * Cleanup guest flows. * Copyrights * Remove unused classname * Match guest view to designs * Add guest screenshots * Update guests * snapshot * Cleanup * fix import * Update tests * More sceenshot fixes * update collapsed * move statements to prevent flake * update snap * Kick it along * Click the room list * Fiddle with the room video list. * More screenshot adjustments * fix imports * fix another import * Update snaps * update snaps * Fix snap flakes * Refactor to move actions to view component, and callbacks to Actions * Cleanup * Cleanup * Cleanup * invert auth * More bits * fix * Change md buttons to sm * Try to assemble the snapshot component of the house of cards * Consistent newlines between tests * Update snapshot Not sure why this was like this, this seems consistet for a logged in user * Update snapshot again these seem sensible for a guest * Remove test I don't really understand why the thing it asserts matters, so I'm removing it for now. * Update snapshot * screenshot * Don't show profile picture for guests I'm not really sure what it meant for this interface to have a property with a default value, so I've removed it and added the property to the view model. * Show avatar in story * update snapshots for showAvatar * Update screenshots & hopefully make hover consistent in one * Use outline home icon --------- Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
@@ -48,6 +48,15 @@
|
||||
"open_dial_pad": "Open dial pad",
|
||||
"separator_label": "Click or drag to expand"
|
||||
},
|
||||
"menus": {
|
||||
"user_menu": {
|
||||
"create_an_account": "Create an account",
|
||||
"got_an_account": "Got an account?",
|
||||
"manage_account": "Manage account",
|
||||
"sign_in": "Sign in",
|
||||
"title": "User menu"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all_messages": "All messages",
|
||||
"default_settings": "Match default settings",
|
||||
@@ -238,6 +247,13 @@
|
||||
"view_image": "View image"
|
||||
}
|
||||
},
|
||||
"user_menu": {
|
||||
"link_new_device": "Link new device",
|
||||
"open_feedback": "Feedback",
|
||||
"open_home": "Home",
|
||||
"open_security": "Security & Privacy",
|
||||
"open_settings": "All settings"
|
||||
},
|
||||
"widget": {
|
||||
"context_menu": {
|
||||
"move_left": "Move left",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -13,6 +14,7 @@ export * from "./core/AvatarWithDetails";
|
||||
export * from "./core/roving";
|
||||
export * from "./room/composer/Banner";
|
||||
export * from "./crypto/SasEmoji";
|
||||
export * from "./menus/UserMenu";
|
||||
export * from "./room/timeline/ReadMarker";
|
||||
export * from "./room/timeline/EventPresentation";
|
||||
export * from "./room/timeline/event-tile/body/EventContentBodyView";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.container {
|
||||
/* As per design, seperator should span whole width. */
|
||||
div[role="separator"] {
|
||||
margin-inline: 0;
|
||||
}
|
||||
max-width: 300px;
|
||||
/* Override menu defaults */
|
||||
padding-block: 0 !important;
|
||||
section.profile {
|
||||
margin: var(--cpd-space-8x) var(--cpd-space-6x);
|
||||
margin-bottom: var(--cpd-space-3x);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-2x);
|
||||
> * {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.displayname {
|
||||
text-wrap: balance;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.createAccount {
|
||||
margin-top: var(--cpd-space-1x);
|
||||
margin-bottom: var(--cpd-space-2x);
|
||||
}
|
||||
}
|
||||
section.actions {
|
||||
margin: var(--cpd-space-2x) 0;
|
||||
margin-bottom: var(--cpd-space-3x);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-1x);
|
||||
font: var(--cpd-font-body-md-medium);
|
||||
> button[data-kind="primary"] > svg {
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.triggerButton {
|
||||
border: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
color: var(--cpd-color-body-primary);
|
||||
gap: var(--cpd-space-2x);
|
||||
> span {
|
||||
height: fit-content;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
margin-top: var(--cpd-space-3x);
|
||||
margin-bottom: var(--cpd-space-4x);
|
||||
margin-left: var(--cpd-space-3x);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Meta, type StoryObj } from "@storybook/react-vite";
|
||||
import React, { type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import { UserMenuView, type UserMenuViewSnapshot, type UserMenuViewActions } from "./UserMenu";
|
||||
import avatarUrl from "../../../static/element.png";
|
||||
import { useMockedViewModel } from "../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../.storybook/withViewDocs";
|
||||
|
||||
const UserMenuWrapperImpl = (snapshot: UserMenuViewSnapshot): JSX.Element => {
|
||||
const vm = useMockedViewModel<UserMenuViewSnapshot, UserMenuViewActions>(snapshot, {
|
||||
setOpen: fn(),
|
||||
createAccount: fn(),
|
||||
signIn: fn(),
|
||||
linkNewDevice: fn(),
|
||||
openFeedback: fn(),
|
||||
openHomePage: fn(),
|
||||
openSecurity: fn(),
|
||||
openSettings: fn(),
|
||||
});
|
||||
return <UserMenuView vm={vm} />;
|
||||
};
|
||||
|
||||
const UserMenuWrapper = withViewDocs(UserMenuWrapperImpl, UserMenuView);
|
||||
|
||||
const meta = {
|
||||
title: "Menus/UserMenu",
|
||||
component: UserMenuWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
open: false,
|
||||
avatarUrl,
|
||||
displayName: "Sally Sanderson",
|
||||
userId: "@person-name:homeserver.com",
|
||||
manageAccountHref: "#",
|
||||
expanded: true,
|
||||
actions: {
|
||||
linkNewDevice: true,
|
||||
openSecurity: true,
|
||||
openFeedback: true,
|
||||
openSettings: true,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=11583-3479&t=DwFpi7Zlq9uJr1SQ-0",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof UserMenuWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const LongerName: Story = {
|
||||
args: {
|
||||
displayName: "Sally Sanderson with a longer name",
|
||||
},
|
||||
};
|
||||
|
||||
export const Open: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
displayName: "Sally Sanderson with a longer name",
|
||||
userId: "@person-name:homeserver.com",
|
||||
expanded: true,
|
||||
showAvatar: true,
|
||||
},
|
||||
parameters: {
|
||||
a11y: {
|
||||
/*
|
||||
* Axe's context parameter
|
||||
* See https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#context-parameter
|
||||
* to learn more.
|
||||
*/
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// Menu contains a header which is invalid
|
||||
id: "aria-required-children",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// Menu pops open by default
|
||||
id: "aria-hidden-focus",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Only used for playwright tests for the menu.
|
||||
// Steals focus if actually opened on the storybook page
|
||||
tags: ["!dev", "!autodocs"],
|
||||
};
|
||||
|
||||
export const Condensed: Story = {
|
||||
args: {
|
||||
displayName: "Sally Sanderson with a longer name",
|
||||
expanded: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Guest: Story = {
|
||||
args: {
|
||||
displayName: "Guest",
|
||||
userId: "@guest:attendees.example.org",
|
||||
manageAccountHref: undefined,
|
||||
showAvatar: false,
|
||||
actions: {
|
||||
createAccount: true,
|
||||
signIn: true,
|
||||
openHomePage: true,
|
||||
openFeedback: true,
|
||||
openSettings: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GuestOpen: Story = {
|
||||
args: {
|
||||
...Guest.args,
|
||||
open: true,
|
||||
},
|
||||
parameters: {
|
||||
a11y: {
|
||||
/*
|
||||
* Axe's context parameter
|
||||
* See https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#context-parameter
|
||||
* to learn more.
|
||||
*/
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// Menu contains a header which is invalid
|
||||
id: "aria-required-children",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// Menu pops open by default
|
||||
id: "aria-hidden-focus",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Only used for playwright tests for the menu.
|
||||
// Steals focus if actually opened on the storybook page
|
||||
tags: ["!dev", "!autodocs"],
|
||||
};
|
||||
|
||||
export const AllOptions: Story = {
|
||||
args: {
|
||||
displayName: "Alice",
|
||||
userId: "@alice:example.org",
|
||||
manageAccountHref: "#",
|
||||
showAvatar: true,
|
||||
actions: {
|
||||
createAccount: true,
|
||||
signIn: true,
|
||||
linkNewDevice: true,
|
||||
openSecurity: true,
|
||||
openHomePage: true,
|
||||
openFeedback: true,
|
||||
openSettings: true,
|
||||
} satisfies Record<keyof UserMenuViewSnapshot["actions"], true>,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { render } from "@test-utils";
|
||||
import React from "react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./UserMenu.stories.tsx";
|
||||
|
||||
const { Default, LongerName, Condensed, Guest } = composeStories(stories);
|
||||
|
||||
describe("UserMenu", () => {
|
||||
it("renders a button", async () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a button with a longer name", async () => {
|
||||
const { container } = render(<LongerName />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders condensed view", async () => {
|
||||
const { container } = render(<Condensed />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a menu", async () => {
|
||||
const { baseElement, getByRole } = render(<Default />);
|
||||
await userEvent.click(getByRole("button"));
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
});
|
||||
it("renders a guest menu", async () => {
|
||||
const { baseElement, getByRole } = render(<Guest />);
|
||||
await userEvent.click(getByRole("button"));
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { Avatar, Button, Link, Menu, MenuItem, Separator, Text } from "@vector-im/compound-web";
|
||||
import {
|
||||
ChatProblemIcon,
|
||||
DevicesIcon,
|
||||
HomeIcon,
|
||||
LockIcon,
|
||||
PopOutIcon,
|
||||
SettingsIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./UserMenu.module.css";
|
||||
import { useViewModel, type ViewModel } from "../../core/viewmodel";
|
||||
import { useI18n } from "../../core/i18n/i18nContext";
|
||||
|
||||
export interface UserMenuViewSnapshot {
|
||||
/**
|
||||
* Is the menu open or closed.
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* Is the menu toggle expanded (avatar + displayname) or collapsed (avatar).
|
||||
*/
|
||||
expanded: boolean;
|
||||
/**
|
||||
* Avatar URL for the user, if one is set.
|
||||
*/
|
||||
avatarUrl?: string;
|
||||
/**
|
||||
* Should the avatar be visible.
|
||||
*/
|
||||
showAvatar?: boolean;
|
||||
/**
|
||||
* Display name for the user.
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* Matrix user ID for the user.
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* Account management URL if the user is using OIDC.
|
||||
*/
|
||||
manageAccountHref?: string;
|
||||
/**
|
||||
* A set of actions that the user can perform from the menu.
|
||||
*/
|
||||
actions: Partial<{
|
||||
createAccount: boolean;
|
||||
signIn: boolean;
|
||||
openHomePage: boolean;
|
||||
linkNewDevice: boolean;
|
||||
openSecurity: boolean;
|
||||
openFeedback: boolean;
|
||||
openSettings: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export declare interface UserMenuViewActions {
|
||||
/**
|
||||
* Called when the menu is opened or closed.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
/**
|
||||
* Called to open the create new account view.
|
||||
*/
|
||||
createAccount: () => void;
|
||||
/**
|
||||
* Called to open the sign in view.
|
||||
*/
|
||||
signIn: () => void;
|
||||
/**
|
||||
* Called to change the view to the configured home page.
|
||||
*/
|
||||
openHomePage: () => void;
|
||||
/**
|
||||
* Called to open the link new device flow.
|
||||
*/
|
||||
linkNewDevice: () => void;
|
||||
/**
|
||||
* Called to open the security tab of the settings dialog.
|
||||
*/
|
||||
openSecurity: () => void;
|
||||
/**
|
||||
* Called to open the feedback dialog.
|
||||
*/
|
||||
openFeedback: () => void;
|
||||
/**
|
||||
* Called to open the settings dialog.
|
||||
*/
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
export type UserMenuViewProps = {
|
||||
vm: ViewModel<UserMenuViewSnapshot, UserMenuViewActions>;
|
||||
/**
|
||||
* Class name for the container
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function UserMenuView({ vm, className }: UserMenuViewProps): JSX.Element {
|
||||
const { userId, displayName, avatarUrl, expanded, open, manageAccountHref, actions, showAvatar } = useViewModel(vm);
|
||||
const { translate: _t } = useI18n();
|
||||
const trigger = (
|
||||
<button className={classNames(styles.triggerButton)} aria-label={_t("menus|user_menu|title")}>
|
||||
<Avatar id={userId} name={displayName} type="round" size="36px" src={avatarUrl} />
|
||||
{expanded && (
|
||||
<Text type="heading" size="sm" as="span" weight="semibold">
|
||||
{displayName}
|
||||
</Text>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
showTitle={false}
|
||||
title={_t("menus|user_menu|title")}
|
||||
trigger={trigger}
|
||||
onOpenChange={vm.setOpen}
|
||||
align="start"
|
||||
side="bottom"
|
||||
className={classNames(styles.container, className)}
|
||||
>
|
||||
<section className={styles.profile}>
|
||||
{showAvatar && <Avatar id={userId} name={displayName} type="round" size="88px" src={avatarUrl} />}
|
||||
<Text className={styles.displayname} type="heading" size="md" weight="semibold" as="span">
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text data-testid="userId" size="md" as="span" type="body">
|
||||
{userId}
|
||||
</Text>
|
||||
{manageAccountHref && (
|
||||
<Button as="a" size="md" kind="tertiary" href={manageAccountHref} Icon={PopOutIcon}>
|
||||
{_t("menus|user_menu|manage_account")}
|
||||
</Button>
|
||||
)}
|
||||
{actions.createAccount && (
|
||||
<Button
|
||||
className={styles.createAccount}
|
||||
size="md"
|
||||
as="button"
|
||||
kind="primary"
|
||||
onClick={vm.createAccount}
|
||||
>
|
||||
{_t("menus|user_menu|create_an_account")}
|
||||
</Button>
|
||||
)}
|
||||
{actions.signIn && (
|
||||
<Text as="span" weight="medium">
|
||||
{_t("menus|user_menu|got_an_account")}
|
||||
<Link as="button" onClick={vm.signIn}>
|
||||
{_t("menus|user_menu|sign_in")}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</section>
|
||||
<Separator />
|
||||
<section className={styles.actions}>
|
||||
{actions.openHomePage && (
|
||||
<MenuItem Icon={HomeIcon} label={_t("user_menu|open_home")} onSelect={vm.openHomePage} />
|
||||
)}
|
||||
{actions.linkNewDevice && (
|
||||
<MenuItem Icon={DevicesIcon} label={_t("user_menu|link_new_device")} onSelect={vm.linkNewDevice} />
|
||||
)}
|
||||
{actions.openSecurity && (
|
||||
<MenuItem Icon={LockIcon} label={_t("user_menu|open_security")} onSelect={vm.openSecurity} />
|
||||
)}
|
||||
{actions.openFeedback && (
|
||||
<MenuItem Icon={ChatProblemIcon} label={_t("user_menu|open_feedback")} onSelect={vm.openFeedback} />
|
||||
)}
|
||||
{actions.openSettings && (
|
||||
<MenuItem Icon={SettingsIcon} label={_t("user_menu|open_settings")} onSelect={vm.openSettings} />
|
||||
)}
|
||||
</section>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UserMenu > renders a button 1`] = `
|
||||
<div>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-_r_0_"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Sally Sanderson
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UserMenu > renders a button with a longer name 1`] = `
|
||||
<div>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-_r_2_"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Sally Sanderson with a longer name
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UserMenu > renders a guest menu 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-_r_8_"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@guest:attendees.example.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="4"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Guest
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`UserMenu > renders a menu 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-_r_6_"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Sally Sanderson
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`UserMenu > renders condensed view 1`] = `
|
||||
<div>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-_r_4_"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export {
|
||||
UserMenuView as UserMenu,
|
||||
type UserMenuViewProps,
|
||||
type UserMenuViewSnapshot as UserMenuSnapshot,
|
||||
type UserMenuViewActions,
|
||||
} from "./UserMenu";
|
||||
Reference in New Issue
Block a user