Move hidden media placeholder to shared components (#33404)

* Move hidden media placeholder to shared components

* Add Snapshots

* Remove legacy hidden media mx class
This commit is contained in:
Zack 2026-05-06 13:23:14 +02:00 committed by GitHub
parent 1cc868a25a
commit b5711264dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 207 additions and 55 deletions

View File

@ -223,7 +223,6 @@
@import "./views/messages/_CreateEvent.pcss";
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_HiddenBody.pcss";
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
@import "./views/messages/_LegacyCallEvent.pcss";
@import "./views/messages/_MFileBody.pcss";
@import "./views/messages/_MImageBody.pcss";

View File

@ -1,29 +0,0 @@
.mx_HiddenMediaPlaceholder {
border: none;
width: 100%;
height: 100%;
inset: 0;
/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: $header-panel-bg-color;
> div {
color: $accent;
/* Icon alignment */
display: flex;
> svg {
margin-top: auto;
margin-bottom: auto;
}
}
}
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
background-color: $background;
}

View File

@ -1,24 +0,0 @@
/*
Copyright 2025 New Vector 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 PropsWithChildren, type MouseEventHandler } from "react";
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
interface IProps {
onClick: MouseEventHandler<HTMLButtonElement>;
}
export const HiddenMediaPlaceholder: React.FunctionComponent<PropsWithChildren<IProps>> = ({ onClick, children }) => {
return (
<button onClick={onClick} className="mx_HiddenMediaPlaceholder">
<div>
<VisibilityOnIcon />
<span>{children}</span>
</div>
</button>
);
};

View File

@ -16,6 +16,7 @@ import { ClientEvent } from "matrix-js-sdk/src/matrix";
import { type ImageContent } from "matrix-js-sdk/src/types";
import { Tooltip } from "@vector-im/compound-web";
import { ImageErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { HiddenMediaPlaceholder } from "@element-hq/web-shared-components";
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
@ -33,7 +34,6 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
import { createReconnectedListener } from "../../../utils/connection";
import MediaProcessingError from "./shared/MediaProcessingError";
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
import { useMediaVisible } from "../../../hooks/useMediaVisible";
import { isMimeTypeAllowed } from "../../../utils/blobs.ts";
import { FileBodyFactory, renderMBody } from "./MBodyFactory";

View File

@ -18,6 +18,7 @@ export * from "./menus/UserMenu";
export * from "./room/timeline/ReadMarker";
export * from "./room/timeline/EventPresentation";
export * from "./room/timeline/event-tile/body/EventContentBodyView";
export * from "./room/timeline/event-tile/body/HiddenMediaPlaceholder";
export * from "./room/timeline/event-tile/body/RedactedBodyView";
export * from "./room/timeline/event-tile/body/MFileBodyView";
export * from "./room/timeline/event-tile/body/MImageBodyView";

View File

@ -0,0 +1,36 @@
/*
* 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.
*/
.button {
border: none;
width: 100%;
height: 100%;
padding: var(--cpd-space-0x);
inset: var(--cpd-space-0x);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: var(--cpd-color-bg-subtle-secondary);
}
.button:hover,
.button:focus-visible {
background-color: var(--cpd-color-bg-canvas-default);
}
.content {
color: var(--cpd-color-text-action-accent);
display: inline-flex;
align-items: center;
gap: var(--cpd-space-1x);
}
.icon {
flex: 0 0 auto;
}

View File

@ -0,0 +1,38 @@
/*
* 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 { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
const HiddenMediaPlaceholderWrapper = withViewDocs(HiddenMediaPlaceholder, HiddenMediaPlaceholder);
const meta = {
title: "Timeline/Timeline Body/HiddenMediaPlaceholder",
component: HiddenMediaPlaceholderWrapper,
tags: ["autodocs"],
args: {
children: "Show image",
onClick: fn(),
className: "",
},
decorators: [
(Story) => (
<div style={{ width: 320, height: 180 }}>
<Story />
</div>
),
],
} satisfies Meta<typeof HiddenMediaPlaceholderWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@ -0,0 +1,47 @@
/*
* 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, screen } from "@test-utils";
import React from "react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
import * as stories from "./HiddenMediaPlaceholder.stories";
const { Default } = composeStories(stories);
describe("HiddenMediaPlaceholder", () => {
it("renders the default story", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
expect(screen.getByRole("button", { name: "Show image" })).toBeInTheDocument();
});
it("invokes the click handler", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<HiddenMediaPlaceholder onClick={onClick}>Show image</HiddenMediaPlaceholder>);
await user.click(screen.getByRole("button", { name: "Show image" }));
expect(onClick).toHaveBeenCalledTimes(1);
});
it("applies a custom className to the root button", () => {
render(
<HiddenMediaPlaceholder onClick={vi.fn()} className="custom-hidden-media">
Show image
</HiddenMediaPlaceholder>,
);
expect(screen.getByRole("button", { name: "Show image" })).toHaveClass("custom-hidden-media");
});
});

View File

@ -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 classNames from "classnames";
import React, { type JSX, type MouseEventHandler, type PropsWithChildren } from "react";
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import styles from "./HiddenMediaPlaceholder.module.css";
export type HiddenMediaPlaceholderProps = PropsWithChildren<{
/**
* CSS class names applied to the root button.
*/
className?: string;
/**
* Invoked when the user chooses to reveal the hidden media.
*/
onClick: MouseEventHandler<HTMLButtonElement>;
}>;
/**
* Renders a full-frame button used to reveal hidden media previews.
*/
export function HiddenMediaPlaceholder({
className,
onClick,
children,
}: Readonly<HiddenMediaPlaceholderProps>): JSX.Element {
return (
<button type="button" onClick={onClick} className={classNames(styles.button, className)}>
<span className={styles.content}>
<VisibilityOnIcon className={styles.icon} aria-hidden="true" />
<span>{children}</span>
</span>
</button>
);
}

View File

@ -0,0 +1,35 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`HiddenMediaPlaceholder > renders the default story 1`] = `
<div>
<div
style="width: 320px; height: 180px;"
>
<button
class="HiddenMediaPlaceholder-module_button"
type="button"
>
<span
class="HiddenMediaPlaceholder-module_content"
>
<svg
aria-hidden="true"
class="HiddenMediaPlaceholder-module_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16q1.875 0 3.188-1.312Q16.5 13.375 16.5 11.5t-1.312-3.187T12 7 8.813 8.313 7.5 11.5t1.313 3.188T12 16m0-1.8q-1.125 0-1.912-.787A2.6 2.6 0 0 1 9.3 11.5q0-1.125.787-1.912A2.6 2.6 0 0 1 12 8.8q1.125 0 1.912.787.788.788.788 1.913t-.787 1.912A2.6 2.6 0 0 1 12 14.2m0 4.8q-3.475 0-6.35-1.837Q2.775 15.324 1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.775.8.8 0 0 1 .1-.313q1.475-3.125 4.35-4.962Q8.525 4 12 4t6.35 1.838T22.7 10.8a.8.8 0 0 1 .1.313 3 3 0 0 1 0 .774.8.8 0 0 1-.1.313q-1.475 3.125-4.35 4.963Q15.475 19 12 19m0-2a9.54 9.54 0 0 0 5.188-1.488A9.77 9.77 0 0 0 20.8 11.5a9.77 9.77 0 0 0-3.613-4.012A9.54 9.54 0 0 0 12 6a9.55 9.55 0 0 0-5.187 1.487A9.77 9.77 0 0 0 3.2 11.5a9.77 9.77 0 0 0 3.613 4.012A9.54 9.54 0 0 0 12 17"
/>
</svg>
<span>
Show image
</span>
</span>
</button>
</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 { HiddenMediaPlaceholder, type HiddenMediaPlaceholderProps } from "./HiddenMediaPlaceholder";