Phase 2 Refactor MImageBody to MVVM and remove legacy component (#33212)

* MVVMing of MImageBody and removing legacy component + css

* Fix Prettier

* update small image to large image in test

* Update test

* Preserve MImageBody legacy class names

* Click image in custom component download test

* Update snapshots

* Update MBodyFactory snapshots

* Added new tests to pass coverage

* Fix prettier

* Remove legacy import that was removed

* Add MImageReplayBody test for coverage

* Remove legacy MImageBody selectors from image view

* Update image body selectors in Playwright tests

* Keep file panel image body spacing compact

* Update apps/web/src/viewmodels/message-body/ImageBodyViewModel.ts

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* added documentation to component

* Fix hidden media placeholder import

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
Zack
2026-05-13 06:03:43 +00:00
committed by GitHub
co-authored by Florian Duros
parent 13dd1a0b5e
commit 1e7c9f672a
26 changed files with 3389 additions and 1608 deletions
@@ -112,6 +112,38 @@ describe("ImageBodyView", () => {
expect(onLinkClick).toHaveBeenCalledTimes(1);
});
it("merges supplied class names with module classes", () => {
const vm = new TestImageBodyViewModel({
state: ImageBodyViewState.READY,
alt: "Custom class image",
src: "https://example.org/full.png",
thumbnailSrc: "https://example.org/thumb.png",
maxWidth: 320,
maxHeight: 240,
aspectRatio: "4 / 3",
});
const { container } = render(
<ImageBodyView
vm={vm}
className="customRoot"
containerClassName="customContainer"
imageClassName="customImage"
/>,
);
const rootClassName = container.querySelector(".customRoot")?.className;
const containerClassName = container.querySelector(".customContainer")?.className;
const imageClassName = screen.getByRole("img", { name: "Custom class image" }).className;
expect(rootClassName).toContain("customRoot");
expect(rootClassName).not.toBe("customRoot");
expect(containerClassName).toContain("customContainer");
expect(containerClassName).not.toBe("customContainer");
expect(imageClassName).toContain("customImage");
expect(imageClassName).not.toBe("customImage");
});
it("swaps to the full source on hover for animated previews", async () => {
const user = userEvent.setup();
const vm = new TestImageBodyViewModel({
@@ -12,6 +12,7 @@ import React, {
type MouseEventHandler,
type PropsWithChildren,
type ReactEventHandler,
type Ref,
useState,
} from "react";
import classNames from "classnames";
@@ -148,6 +149,18 @@ interface ImageBodyViewProps {
* Optional host CSS class.
*/
className?: string;
/**
* Optional CSS class applied to the media frame container.
*/
containerClassName?: string;
/**
* Optional CSS class applied to the rendered image element.
*/
imageClassName?: string;
/**
* Optional ref to the rendered image element.
*/
imageRef?: Ref<HTMLImageElement>;
/**
* Optional supplemental content rendered after the media frame.
*/
@@ -202,7 +215,14 @@ function renderPlaceholder({
* </ImageBodyView>
* ```
*/
export function ImageBodyView({ vm, className, children }: Readonly<ImageBodyViewProps>): JSX.Element {
export function ImageBodyView({
vm,
className,
containerClassName,
imageClassName,
imageRef,
children,
}: Readonly<ImageBodyViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const {
state,
@@ -230,6 +250,8 @@ export function ImageBodyView({ vm, className, children }: Readonly<ImageBodyVie
const hoverOrFocus = hover || focus;
const rootClassName = classNames(className, styles.root);
const resolvedContainerClassName = classNames(containerClassName, styles.thumbnailContainer);
const resolvedImageClassName = classNames(imageClassName, styles.image);
if (state === ImageBodyViewState.ERROR) {
return (
@@ -281,9 +303,10 @@ export function ImageBodyView({ vm, className, children }: Readonly<ImageBodyVie
</div>
) : resolvedImageSrc ? (
<img
className={styles.image}
className={resolvedImageClassName}
src={resolvedImageSrc}
alt={alt}
ref={imageRef}
onError={vm.onImageError}
onLoad={vm.onImageLoad}
onMouseEnter={(): void => setHover(true)}
@@ -302,7 +325,7 @@ export function ImageBodyView({ vm, className, children }: Readonly<ImageBodyVie
) : null;
let frame = (
<div className={styles.thumbnailContainer} style={containerStyle}>
<div className={resolvedContainerClassName} style={containerStyle}>
{showPlaceholder && (
<div
className={classNames(styles.placeholder, {