Move EventTileBubble to shared components (#31911)

* Move EventTileBubble to shared components as is

* Added documentation and updated stories and unit tests

* Move 'global' element web css to _common.pcss

* Adding playwright snapshots

* Updated comments

* Added legacy mx_MessageTimestamp class and updated snapshots

* Regenerate snapshots with correct hash

* Changes to css and removed timestamp from properties after review.

* Update screenshot for room-list and fix flaky CI playwright test.

* Blur the play button before matching screenshots

* Changed to button focused instead of blur for consistancy

* Stabilize play button appearance in CI (disabled due to decoding)

* Force play button appearance in CI (disabled due to decoding)

* Add comments on playwright test changes.
Change from React.RefObject<any> to Ref<HTMLDivElement> in EncryptionEvent.tsx

* Update playwright/e2e/composer/CIDER.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update playwright/e2e/composer/CIDER.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update playwright/e2e/crypto/toasts.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
rbondesson
2026-02-03 15:37:57 +01:00
committed by GitHub
parent eb909f1090
commit a1be203683
76 changed files with 463 additions and 180 deletions

View File

@@ -0,0 +1,51 @@
/*
* 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 {
background-color: var(--cpd-color-bg-subtle-secondary);
padding: var(--cpd-space-3x);
border-radius: 8px;
/* Reserve space for external timestamps, but also cap the width */
/* Legacy variable: --MessageTimestamp-width: 46px; /* 8 + 30 (avatar) + 8 */
/* max-width: min(calc(100% - 2 * var(--MessageTimestamp-width)), 600px); */
max-width: min(calc(100% - 2 * 46px), 600px);
box-sizing: border-box;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content min-content;
svg {
position: relative;
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
inset: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
margin-top: var(--cpd-space-1x);
}
.title,
.subtitle {
grid-column: 2;
overflow-wrap: break-word;
min-inline-size: 50px;
}
.title {
font-weight: var(--cpd-font-weight-semibold);
font-size: var(--cpd-font-size-body-md);
grid-row: 1;
}
.subtitle {
font-size: var(--cpd-font-size-body-sm);
grid-row: 2;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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 from "react";
import { LockSolidIcon, ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { EventTileBubble } from "./EventTileBubble";
export default {
title: "Event/EventTileBubble",
component: EventTileBubble,
tags: ["autodocs"],
args: {
icon: <ErrorSolidIcon />,
title: "Title goes here",
subtitle: "Subtitle goes here",
className: "custom-class",
},
} as Meta<typeof EventTileBubble>;
const Template: StoryFn<typeof EventTileBubble> = (args) => <EventTileBubble {...args} />;
export const Default = Template.bind({});
export const HasLockSolidIcon = Template.bind({});
HasLockSolidIcon.args = {
className: undefined,
icon: <LockSolidIcon />,
children: undefined,
};
export const HasChildren = Template.bind({});
HasChildren.args = {
className: undefined,
children: <div>children</div>,
};
export const IsCryptoEventBubble = Template.bind({});
IsCryptoEventBubble.args = {
className: undefined,
icon: <LockSolidIcon />,
title: "Encryption enabled",
subtitle: "Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture.",
};

View File

@@ -0,0 +1,37 @@
/*
* 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 { render } from "@test-utils";
import { composeStories } from "@storybook/react-vite";
import { describe, it, expect } from "vitest";
import React from "react";
import * as stories from "./EventTileBubble.stories.tsx";
const { Default, HasLockSolidIcon, HasChildren, IsCryptoEventBubble } = composeStories(stories);
describe("EventTileBubble", () => {
it("renders the event tile bubble", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble with icon", () => {
const { container } = render(<HasLockSolidIcon />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble with children", () => {
const { container } = render(<HasChildren />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble as crypto event bubble", () => {
const { container } = render(<IsCryptoEventBubble />);
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,64 @@
/*
* 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, type ReactNode } from "react";
import classNames from "classnames";
import styles from "./EventTileBubble.module.css";
export interface EventTileBubbleProps {
/**
* Icon rendered at the start of the bubble.
*/
icon: JSX.Element;
/**
* Main title text for the bubble.
*/
title: string;
/**
* Optional subtitle rendered beneath the title.
*/
subtitle?: ReactNode;
/**
* Optional extra class name for the container.
*/
className?: string;
/**
* Optional children rendered between subtitle and timestamp.
*/
children?: JSX.Element;
/**
* Forwarded ref for the container element.
*/
ref?: React.RefObject<HTMLDivElement>;
}
/**
* EventTileBubble renders a compact event tile with an icon, title, and optional subtitle/content.
*
* @example
* ```tsx
* <EventTileBubble icon={<Icon />} title="Room created" />
* ```
*/
export function EventTileBubble({
icon,
title,
subtitle,
className,
children,
ref,
}: EventTileBubbleProps): JSX.Element {
return (
<div className={classNames(styles.container, className)} ref={ref}>
{icon}
<div className={styles.title}>{title}</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
{children}
</div>
);
}

View File

@@ -0,0 +1,124 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EventTileBubble > renders the event tile bubble 1`] = `
<div>
<div
class="container custom-class"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble as crypto event bubble 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="title"
>
Encryption enabled
</div>
<div
class="subtitle"
>
Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture.
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble with children 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
<div>
children
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble with icon 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,8 @@
/*
* 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 { EventTileBubble, type EventTileBubbleProps } from "./EventTileBubble";

View File

@@ -13,6 +13,7 @@ export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/EventTileBubble";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./message-body/DecryptionFailureBodyView";