Make shared-components tiles render identically outside Element Web - # 2 (#33485)

* Adding correct layout/density attributes to unit tests

* Move app/web styling to FileBodyView

* Unify styling for MediaBody wrapped components

* Fix prettier issues

* Update app/web snapshots

* Wrap file download and audio loading states in MediaBody

* Revert the MediaBody for download buttons.

* Revert changes for MediaBody wrappers

* Updated snaphots for MediaBody components
This commit is contained in:
rbondesson
2026-05-18 05:59:03 +00:00
committed by GitHub
parent 7fe8cdafe0
commit 297ad9bb8d
33 changed files with 63 additions and 89 deletions
-2
View File
@@ -223,12 +223,10 @@
@import "./views/messages/_CreateEvent.pcss"; @import "./views/messages/_CreateEvent.pcss";
@import "./views/messages/_DisambiguatedProfile.pcss"; @import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_LegacyCallEvent.pcss"; @import "./views/messages/_LegacyCallEvent.pcss";
@import "./views/messages/_MFileBody.pcss";
@import "./views/messages/_MImageReplyBody.pcss"; @import "./views/messages/_MImageReplyBody.pcss";
@import "./views/messages/_MLocationBody.pcss"; @import "./views/messages/_MLocationBody.pcss";
@import "./views/messages/_MPollBody.pcss"; @import "./views/messages/_MPollBody.pcss";
@import "./views/messages/_MStickerBody.pcss"; @import "./views/messages/_MStickerBody.pcss";
@import "./views/messages/_MediaBody.pcss";
@import "./views/messages/_MessageActionBar.pcss"; @import "./views/messages/_MessageActionBar.pcss";
@import "./views/messages/_ReactionsRow.pcss"; @import "./views/messages/_ReactionsRow.pcss";
@import "./views/messages/_TextualEvent.pcss"; @import "./views/messages/_TextualEvent.pcss";
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
/* are shared amongst multiple voice message components. */ /* are shared amongst multiple voice message components. */
/* Container for live recording and playback controls */ /* Container for live recording and playback controls */
.mx_MediaBody.mx_VoiceMessagePrimaryContainer { .mx_VoiceMessagePrimaryContainer {
/* Match mx_MediaBody spacing, offsetting the waveform's 1px internal right padding. */ /* Match mx_MediaBody spacing, offsetting the waveform's 1px internal right padding. */
padding: 6px 11px 6px 12px; padding: 6px 11px 6px 12px;
@@ -1,27 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
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.
*/
.mx_MFileBody [data-type="download"] {
height: var(--cpd-space-9x);
& object {
margin-left: -16px;
padding-right: 4px;
margin-top: -4px;
vertical-align: middle;
pointer-events: none;
}
}
.mx_MFileBody [data-type="info"] {
svg {
color: $secondary-content !important;
background-color: $system !important;
border-radius: 20px !important;
}
}
@@ -1,24 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
/* A "media body" is any file upload looking thing, apart from images and videos (they */
/* have unique styles). */
.mx_MediaBody {
background-color: $panels;
border-radius: 12px;
max-width: 243px; /* use max-width instead of width so it fits within right panels */
color: $secondary-content;
font: var(--cpd-font-body-md-regular);
line-height: $font-24px;
}
.mx_MAudioBody > .mx_MediaBody {
border-radius: var(--MBody-border-radius);
}
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { type ReactNode } from "react"; import React, { type ReactNode } from "react";
import { MediaBody } from "@element-hq/web-shared-components";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
@@ -74,8 +75,8 @@ export default class RecordingPlayback extends AudioPlayerBase<IProps> {
} }
return ( return (
<div <MediaBody
className="mx_MediaBody mx_VoiceMessagePrimaryContainer" className="mx_VoiceMessagePrimaryContainer"
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
data-testid="recording-playback" data-testid="recording-playback"
> >
@@ -85,7 +86,7 @@ export default class RecordingPlayback extends AudioPlayerBase<IProps> {
ref={this.playPauseRef} ref={this.playPauseRef}
/> />
{body} {body}
</div> </MediaBody>
); );
} }
} }
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { type ReactNode } from "react"; import React, { type ReactNode } from "react";
import { MediaBody } from "@element-hq/web-shared-components";
import { type Room, type IEventRelation, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type Room, type IEventRelation, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { DeleteIcon, StopSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { DeleteIcon, StopSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
@@ -239,10 +240,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// only other UI is the recording-in-progress UI // only other UI is the recording-in-progress UI
return ( return (
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording"> <MediaBody className="mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
<LiveRecordingClock recorder={this.state.recorder} /> <LiveRecordingClock recorder={this.state.recorder} />
<LiveRecordingWaveform recorder={this.state.recorder} /> <LiveRecordingWaveform recorder={this.state.recorder} />
</div> </MediaBody>
); );
} }
@@ -3,10 +3,10 @@
exports[`MBodyFactory renderMBody fallback shows m.audio generic placeholder when showFileInfo is true 1`] = ` exports[`MBodyFactory renderMBody fallback shows m.audio generic placeholder when showFileInfo is true 1`] = `
<div> <div>
<span <span
class="_content_1t2mx_8 mx_MFileBody" class="_content_bibip_8 mx_MFileBody"
> >
<div <div
class="mx_MediaBody _mediaBody_rgndh_8" class="mx_MediaBody _mediaBody_14jys_8"
data-type="info" data-type="info"
> >
<button <button
@@ -44,10 +44,10 @@ exports[`MBodyFactory renderMBody fallback shows m.audio generic placeholder whe
exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when showFileInfo is true 1`] = ` exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when showFileInfo is true 1`] = `
<div> <div>
<span <span
class="_content_1t2mx_8 mx_MFileBody" class="_content_bibip_8 mx_MFileBody"
> >
<div <div
class="mx_MediaBody _mediaBody_rgndh_8" class="mx_MediaBody _mediaBody_14jys_8"
data-type="info" data-type="info"
> >
<button <button
@@ -82,7 +82,7 @@ exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when
exports[`MBodyFactory renderMBody renders download button for m.file in file rendering type 1`] = ` exports[`MBodyFactory renderMBody renders download button for m.file in file rendering type 1`] = `
<div> <div>
<span <span
class="_content_1t2mx_8 mx_MFileBody" class="_content_bibip_8 mx_MFileBody"
> >
<div <div
data-type="download" data-type="download"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

@@ -29,7 +29,9 @@ describe("TileErrorView", () => {
}); });
it("renders the bubble layout variant", () => { it("renders the bubble layout variant", () => {
const { container } = render(<BubbleLayout />); const { container } = render(<BubbleLayout />, {
presentation: { layout: "bubble" },
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
@@ -6,7 +6,7 @@ exports[`TileErrorView > renders the bubble layout variant 1`] = `
<li <li
class="TileErrorView-module_tileErrorView" class="TileErrorView-module_tileErrorView"
data-event-density="default" data-event-density="default"
data-event-layout="group" data-event-layout="bubble"
> >
<div <div
class="TileErrorView-module_line" class="TileErrorView-module_line"
@@ -28,7 +28,9 @@ describe("UrlPreviewGroupView", () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it("renders with compact density", () => { it("renders with compact density", () => {
const { container } = render(<WithCompactView />); const { container } = render(<WithCompactView />, {
presentation: { density: "compact" },
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
}); });
@@ -342,7 +342,7 @@ exports[`UrlPreviewGroupView > renders with compact density 1`] = `
<div> <div>
<div <div
class="UrlPreviewGroupView-module_wrapper" class="UrlPreviewGroupView-module_wrapper"
data-event-density="default" data-event-density="compact"
data-event-layout="group" data-event-layout="group"
> >
<div <div
@@ -7,7 +7,6 @@
.audioPlayer { .audioPlayer {
padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x); padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x);
border-radius: 8px;
} }
.mediaInfo { .mediaInfo {
@@ -39,6 +39,9 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
padding: var(--cpd-space-2x); padding: var(--cpd-space-2x);
color: var(--cpd-color-text-secondary);
background-color: var(--cpd-color-bg-subtle-primary);
border-radius: 20px;
} }
& span { & span {
@@ -57,6 +60,7 @@
.content [data-type="download"] { .content [data-type="download"] {
align-items: center; align-items: center;
height: var(--cpd-space-9x);
& iframe { & iframe {
margin: 0; margin: 0;
@@ -23,7 +23,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
contain: paint; contain: paint;
border-radius: var(--MBody-border-radius); border-radius: 8px;
} }
.placeholder { .placeholder {
@@ -70,7 +70,7 @@
max-width: min(100%, 350px); max-width: min(100%, 350px);
overflow: hidden; overflow: hidden;
padding: var(--cpd-space-1x); padding: var(--cpd-space-1x);
border-radius: var(--MBody-border-radius); border-radius: 8px;
background-color: rgb(0 0 0 / 0.6); background-color: rgb(0 0 0 / 0.6);
color: #fff; color: #fff;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -4,7 +4,7 @@
.container { .container {
overflow: hidden; overflow: hidden;
border-radius: var(--MBody-border-radius); border-radius: 8px;
} }
.video { .video {
@@ -6,12 +6,13 @@
*/ */
.mediaBody { .mediaBody {
color: var(--cpd-color-text-secondary);
background-color: var(--cpd-color-bg-subtle-secondary); background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: 12px; border-radius: 8px;
max-width: 243px; /* use max-width instead of width so it fits within right panels */ max-width: 243px; /* use max-width instead of width so it fits within right panels */
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-secondary);
padding: var(--cpd-space-1-5x) var(--cpd-space-3x); padding: var(--cpd-space-1-5x) var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);
line-height: var(--cpd-font-size-heading-md);
} }
@@ -16,32 +16,49 @@ import React, { type ReactElement } from "react";
import { render, type RenderOptions } from "@testing-library/react"; import { render, type RenderOptions } from "@testing-library/react";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { I18nApi, I18nContext } from "../.."; import {
DEFAULT_EVENT_PRESENTATION,
EventPresentationProvider,
I18nApi,
I18nContext,
type EventPresentation,
} from "../..";
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { type SharedRenderOptions = RenderOptions & {
presentation?: Partial<EventPresentation>;
};
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"], presentation?: Partial<EventPresentation>) => {
return ({ children }: { children: React.ReactNode }) => { return ({ children }: { children: React.ReactNode }) => {
const resolvedPresentation: EventPresentation | undefined = presentation
? { ...DEFAULT_EVENT_PRESENTATION, ...presentation }
: undefined;
const content = resolvedPresentation ? (
<EventPresentationProvider value={resolvedPresentation}>
<TooltipProvider>{children}</TooltipProvider>
</EventPresentationProvider>
) : (
<TooltipProvider>{children}</TooltipProvider>
);
if (Wrapper) { if (Wrapper) {
return ( return (
<I18nContext.Provider value={new I18nApi()}> <I18nContext.Provider value={new I18nApi()}>
<Wrapper> <Wrapper>{content}</Wrapper>
<TooltipProvider>{children}</TooltipProvider>
</Wrapper>
</I18nContext.Provider> </I18nContext.Provider>
); );
} else { } else {
return ( return <I18nContext.Provider value={new I18nApi()}>{content}</I18nContext.Provider>;
<I18nContext.Provider value={new I18nApi()}>
<TooltipProvider>{children}</TooltipProvider>
</I18nContext.Provider>
);
} }
}; };
}; };
const customRender = (ui: ReactElement, options: RenderOptions = {}): ReturnType<typeof render> => { const customRender = (ui: ReactElement, options: SharedRenderOptions = {}): ReturnType<typeof render> => {
const { presentation, wrapper, ...renderOptions } = options;
return render(ui, { return render(ui, {
...options, ...renderOptions,
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"], wrapper: wrapWithTooltipProvider(wrapper, presentation) as RenderOptions["wrapper"],
}) as ReturnType<typeof render>; }) as ReturnType<typeof render>;
}; };