Ensure interface gradually reduces visible buttons when viewport shrinks (#33477)
* Rewrite Measured to be a functional component * Add tests to cover narrow viewports * lint * breakpoint is optional * Cleanup * Provide default value * Fixup * fix two snaps * Update screenshot * and the other one * unfake CIDER * Update snaps AGAIN
@ -77,6 +77,12 @@ test.describe("Composer", () => {
|
||||
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("renders in narrow viewports", { tag: "@screenshot" }, async ({ page, bot, app }) => {
|
||||
// Shrink the viewport
|
||||
await page.setViewportSize({ width: 500, height: 1080 });
|
||||
await expect(app.getComposer()).toMatchScreenshot("narrow.png");
|
||||
});
|
||||
|
||||
test.describe("render emoji picker with larger viewport height", async () => {
|
||||
test.use({ viewport: { width: 1280, height: 720 } });
|
||||
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
@ -201,12 +207,6 @@ test.describe("Composer", () => {
|
||||
});
|
||||
|
||||
test("can paste a file", async ({ page, bot, app }) => {
|
||||
// Set up a private room so we have another user to mention
|
||||
await app.client.createRoom({
|
||||
is_direct: true,
|
||||
invite: [bot.credentials.userId],
|
||||
});
|
||||
await app.viewRoomByName("Bob");
|
||||
await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png");
|
||||
await expect(page.locator(".mx_ImageBody")).toBeVisible();
|
||||
});
|
||||
|
||||
@ -216,6 +216,12 @@ test.describe("Composer", () => {
|
||||
await expect(page.locator(".mx_ImageBody")).toBeVisible();
|
||||
});
|
||||
|
||||
test("renders in narrow viewports", { tag: "@screenshot" }, async ({ page, bot, app }) => {
|
||||
// Shrink the viewport
|
||||
await page.setViewportSize({ width: 750, height: 1080 });
|
||||
await expect(page.locator(".mx_MessageComposer_wrapper")).toMatchScreenshot("narrow.png");
|
||||
});
|
||||
|
||||
test.describe("when Control+Enter is required to send", () => {
|
||||
test.beforeEach(async ({ app }) => {
|
||||
await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
||||
|
||||
@ -327,19 +327,19 @@ test.describe("Threads", () => {
|
||||
|
||||
test.describe("with larger viewport", async () => {
|
||||
// Increase viewport size so that voice messages fit
|
||||
test.use({ viewport: { width: 1280, height: 720 } });
|
||||
test.use({ viewport: { width: 1440, height: 720 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Increase right-panel size, so that voice messages fit
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("mx_rhs_size", "600");
|
||||
window.localStorage.setItem("mx_rhs_size", "700");
|
||||
});
|
||||
});
|
||||
|
||||
test("can send voice messages", { tag: ["@no-firefox", "@no-webkit"] }, async ({ page, app, user }) => {
|
||||
// Increase right-panel size, so that voice messages fit
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.setItem("mx_rhs_size", "600");
|
||||
window.localStorage.setItem("mx_rhs_size", "700");
|
||||
});
|
||||
|
||||
const roomId = await app.client.createRoom({});
|
||||
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@ -435,7 +435,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
|
||||
}}
|
||||
>
|
||||
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
|
||||
<Measured breakpoint={400} sensor={this.card} onMeasurement={this.onMeasurement} />
|
||||
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
|
||||
|
||||
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2026 Element Creations Ltd
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@ -6,57 +7,65 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type RefObject } from "react";
|
||||
import { useEffect, useRef, type RefObject } from "react";
|
||||
|
||||
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* Element to watch for resize changes on.
|
||||
*/
|
||||
sensor: RefObject<Element | null>;
|
||||
breakpoint: number;
|
||||
onMeasurement(narrow: boolean): void;
|
||||
/**
|
||||
* Minimum width of element to be considered full-size.
|
||||
* Defaults to `500px`
|
||||
*/
|
||||
breakpoint?: number;
|
||||
/**
|
||||
* Callback for when the narrowness property changes.
|
||||
* @param narrow
|
||||
* @returns
|
||||
*/
|
||||
onMeasurement: (narrow: boolean) => void;
|
||||
}
|
||||
|
||||
export default class Measured extends React.PureComponent<IProps> {
|
||||
private static instanceCount = 0;
|
||||
private readonly instanceId: number;
|
||||
let instanceCount = 0;
|
||||
|
||||
public static defaultProps = {
|
||||
breakpoint: 500,
|
||||
};
|
||||
/**
|
||||
* This component can watch a single element for width changes, and will fire
|
||||
* a callback if the width changes to be lower or higher than the `breakpoint`.
|
||||
*/
|
||||
export default function Measured({ sensor, breakpoint = 500, onMeasurement }: IProps): null {
|
||||
const instanceIdRef = useRef(instanceCount++);
|
||||
const instanceId = instanceIdRef.current;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.instanceId = Measured.instanceCount++;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
UIStore.instance.on(`Measured${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
const previous = prevProps.sensor.current;
|
||||
const current = this.props.sensor.current;
|
||||
if (previous === current) return;
|
||||
if (previous) {
|
||||
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||
useEffect(() => {
|
||||
if (sensor.current) {
|
||||
UIStore.instance.trackElementDimensions(`Measured${instanceId}`, sensor.current);
|
||||
}
|
||||
if (current) {
|
||||
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, current);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
UIStore.instance.off(`Measured${this.instanceId}`, this.onResize);
|
||||
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||
}
|
||||
return () => {
|
||||
UIStore.instance.stopTrackingElementDimensions(`Measured${instanceId}`);
|
||||
};
|
||||
}, [sensor, instanceId]);
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry): void => {
|
||||
if (type !== UI_EVENTS.Resize) return;
|
||||
this.props.onMeasurement(entry.contentRect.width <= this.props.breakpoint);
|
||||
};
|
||||
const narrow = useEventEmitterState<boolean>(
|
||||
UIStore.instance,
|
||||
`Measured${instanceId}`,
|
||||
(_type: unknown, entry: ResizeObserverEntry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
// N.B there is only one `_type` of resize event.
|
||||
return entry.contentRect.width <= breakpoint;
|
||||
},
|
||||
);
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return null;
|
||||
}
|
||||
// Only fire when the state changes.
|
||||
useEffect(() => {
|
||||
onMeasurement(narrow);
|
||||
}, [onMeasurement, narrow]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||