Move E2ePadlock to shared components (#33538)

* Move E2ePadlock to shared components

* Add storybook images

* Move translation to shared components

* Fix E2ePadlock story accessibility

* Update E2eMessageSharedIcon test for padlock role
This commit is contained in:
Zack
2026-05-20 15:20:17 +02:00
committed by GitHub
parent ba3682074b
commit 223a861535
17 changed files with 229 additions and 78 deletions
@@ -221,6 +221,7 @@
},
"download_action_decrypting": "Decrypting",
"download_action_downloading": "Downloading",
"e2e_state": "State of the end-to-end encryption",
"m.audio": {
"audio_player": "Audio player",
"error_downloading_audio": "Error downloading audio",
+1
View File
@@ -40,6 +40,7 @@ export * from "./room/timeline/DateSeparatorView";
export * from "./room/timeline/TimelineSeparator";
export * from "./room/timeline/event-tile/actions/ActionBarView";
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
export * from "./room/timeline/event-tile/EventTileView/E2ePadlock";
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
export * from "./room/timeline/event-tile/call";
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
@@ -0,0 +1,19 @@
/*
* 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.
*/
.e2ePadlock {
position: relative;
width: 14px;
height: 14px;
display: block;
svg {
height: inherit;
width: inherit;
display: block;
}
}
@@ -0,0 +1,45 @@
/*
* 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, StoryObj } from "@storybook/react-vite";
import { E2ePadlock, E2ePadlockIcon } from "./E2ePadlock";
const meta = {
title: "Timeline/Timeline Event/E2ePadlock",
component: E2ePadlock,
tags: ["autodocs"],
argTypes: {
icon: {
options: Object.values(E2ePadlockIcon),
control: { type: "select" },
},
},
args: {
icon: E2ePadlockIcon.Normal,
title: "The authenticity of this message could not be guaranteed",
className: "",
},
} satisfies Meta<typeof E2ePadlock>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Normal: Story = {};
export const Warning: Story = {
args: {
icon: E2ePadlockIcon.Warning,
title: "This message was sent unencrypted",
},
};
export const DecryptionFailure: Story = {
args: {
icon: E2ePadlockIcon.DecryptionFailure,
title: "Unable to decrypt message",
},
};
@@ -0,0 +1,42 @@
/*
* 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 { composeStories } from "@storybook/react-vite";
import { describe, expect, it } from "vitest";
import { render, screen } from "@test-utils";
import { E2ePadlock, E2ePadlockIcon } from "./E2ePadlock";
import * as stories from "./E2ePadlock.stories";
const { Normal, Warning, DecryptionFailure } = composeStories(stories);
describe("E2ePadlock", () => {
it("renders a 'Normal' icon", () => {
const { container } = render(<Normal />);
expect(container).toMatchSnapshot();
});
it("renders a 'Warning' icon", () => {
const { container } = render(<Warning />);
expect(container).toMatchSnapshot();
});
it("renders a 'DecryptionFailure' icon", () => {
const { container } = render(<DecryptionFailure />);
expect(container).toMatchSnapshot();
});
it("applies a custom className to the icon container", () => {
render(<E2ePadlock icon={E2ePadlockIcon.Normal} title="Test tooltip" className="custom-e2e-padlock" />);
expect(screen.getByTestId("e2e-padlock")).toHaveClass("custom-e2e-padlock");
});
});
@@ -0,0 +1,75 @@
/*
* Copyright 2026 Element Creations Ltd.
* Copyright 2025 Vector Creations Ltd.
* Copyright 2024 New Vector Ltd.
* Copyright 2015-2023 The Matrix.org Foundation C.I.C.
* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
*
* 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 classNames from "classnames";
import { ErrorSolidIcon, InfoIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../../../core/i18n/i18n";
import styles from "./E2ePadlock.module.css";
/**
* The icon to display in an {@link E2ePadlock}.
*/
export enum E2ePadlockIcon {
/** Compound Info icon in grey */
Normal = "normal",
/** Compound ErrorSolid icon in red */
Warning = "warning",
/** Compound ErrorSolid icon in grey */
DecryptionFailure = "decryption_failure",
}
export interface E2ePadlockProps {
/** The icon to display. */
icon: E2ePadlockIcon;
/** The tooltip for the icon, displayed on hover. */
title: string;
/** Optional CSS class name applied to the icon container. */
className?: string;
}
const icons: Record<E2ePadlockIcon, JSX.Element> = {
[E2ePadlockIcon.Normal]: <InfoIcon color="var(--cpd-color-icon-tertiary)" />,
[E2ePadlockIcon.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2ePadlockIcon.DecryptionFailure]: <ErrorSolidIcon color="var(--cpd-color-icon-tertiary)" />,
};
/**
* A small icon with tooltip, used in the left margin of an event tile to
* indicate a problem with an encrypted event.
*
* The icon is rendered with `data-testid="e2e-padlock"`.
*/
export function E2ePadlock({ icon, title, className }: Readonly<E2ePadlockProps>): JSX.Element {
// We specify isTriggerInteractive=true and make the div interactive manually as a workaround for
// https://github.com/element-hq/compound/issues/294
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
return (
<Tooltip label={title} isTriggerInteractive={true}>
<div
data-testid="e2e-padlock"
className={classNames(styles.e2ePadlock, className)}
role="img"
tabIndex={0}
aria-label={_t("timeline|e2e_state")}
>
{icons[icon]}
</div>
</Tooltip>
);
/* eslint-enable jsx-a11y/no-noninteractive-tabindex */
}
@@ -0,0 +1,84 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`E2ePadlock > renders a 'DecryptionFailure' icon 1`] = `
<div>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_c_"
class="E2ePadlock-module_e2ePadlock"
data-testid="e2e-padlock"
role="img"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
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>
</div>
`;
exports[`E2ePadlock > renders a 'Normal' icon 1`] = `
<div>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_0_"
class="E2ePadlock-module_e2ePadlock"
data-testid="e2e-padlock"
role="img"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
`;
exports[`E2ePadlock > renders a 'Warning' icon 1`] = `
<div>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_6_"
class="E2ePadlock-module_e2ePadlock"
data-testid="e2e-padlock"
role="img"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
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>
</div>
`;
@@ -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 { E2ePadlock, E2ePadlockIcon, type E2ePadlockProps } from "./E2ePadlock";