mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
fix media queries in component based element call.
This commit is contained in:
@@ -234,6 +234,9 @@ logged along the bottom.
|
||||
The call lays itself out for the size of the element it is mounted in, not the
|
||||
window: a host that shrinks the container to a corner of its page gets the
|
||||
picture-in-picture layout, just as a host that shrank the whole iframe used to.
|
||||
The breakpoints in Element Call's stylesheets are `@container element-call`
|
||||
queries against its root element for the same reason; for the standalone app
|
||||
the root is the page, so they mean what the media queries they replaced did.
|
||||
|
||||
The component's stylesheet is confined to the element it is mounted in: the
|
||||
build rewrites every selector so that it matches only Element Call's root or
|
||||
|
||||
@@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, type Locator, test } from "@playwright/test";
|
||||
import { expect, type Locator, type Page, test } from "@playwright/test";
|
||||
|
||||
import { createUserAndRoom, expectWithin, startHarness } from "./harness.ts";
|
||||
import { SpaHelpers } from "../spa-helpers.ts";
|
||||
|
||||
/**
|
||||
* Element Call embedded as a React component, driven through the development
|
||||
@@ -165,3 +166,82 @@ test("lays itself out for the space it is given, not the page", async ({
|
||||
await resize(900, 700);
|
||||
await expect(call).not.toHaveAttribute("data-layout", "pip");
|
||||
});
|
||||
|
||||
/**
|
||||
* The shape of a call at whatever size it has been given: the layout it chose,
|
||||
* how much of the height the tile and the footer take, and which controls the
|
||||
* footer shows. Two calls with the same shape look the same, participants aside.
|
||||
*/
|
||||
async function callShape(scope: Page | Locator): Promise<{
|
||||
layout: string | null;
|
||||
tileHeight: number;
|
||||
footerHeight: number;
|
||||
buttons: (string | null)[];
|
||||
}> {
|
||||
const call = scope.locator("[data-layout]");
|
||||
const footer = scope.getByTestId("footer-container");
|
||||
await expect(footer).toBeVisible();
|
||||
const tile = scope.getByTestId("videoTile").first();
|
||||
await expect(tile).toBeVisible();
|
||||
const tileBox = (await tile.boundingBox())!;
|
||||
const footerBox = (await footer.boundingBox())!;
|
||||
const buttons = await footer
|
||||
.getByRole("button")
|
||||
.filter({ visible: true })
|
||||
.evaluateAll((elements) =>
|
||||
elements.map((element) => element.getAttribute("aria-label")),
|
||||
);
|
||||
return {
|
||||
layout: await call.getAttribute("data-layout"),
|
||||
tileHeight: Math.round(tileBox.height),
|
||||
footerHeight: Math.round(footerBox.height),
|
||||
buttons,
|
||||
};
|
||||
}
|
||||
|
||||
test("looks the same in a small container as in a small window", async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
// Two calls to set up, one of them through the harness's two logins
|
||||
test.setTimeout(240_000);
|
||||
const size = { width: 300, height: 300 };
|
||||
|
||||
// The reference is Element Call owning a window of that size, which is what
|
||||
// a mobile app's webview or a browser's picture-in-picture gives it, and
|
||||
// what its small-window styling was written for.
|
||||
const referenceContext = await browser.newContext({
|
||||
viewport: size,
|
||||
ignoreHTTPSErrors: true,
|
||||
permissions: ["microphone", "camera"],
|
||||
});
|
||||
const referencePage = await referenceContext.newPage();
|
||||
await referencePage.goto("/");
|
||||
await SpaHelpers.createCall(referencePage, "Reference", "smallwindow", true);
|
||||
const reference = await callShape(referencePage);
|
||||
await referencePage.screenshot({
|
||||
path: test.info().outputPath("small-window.png"),
|
||||
});
|
||||
|
||||
// The component gets a container of that size, in a window that is far larger
|
||||
const { username, roomId } = await createUserAndRoom("smallcontainer");
|
||||
const panes = await startHarness(page, username, roomId);
|
||||
const pane = panes.first();
|
||||
const container = pane.getByTestId("call-container");
|
||||
await container.evaluate((element, { width, height }) => {
|
||||
element.style.width = `${width}px`;
|
||||
element.style.height = `${height}px`;
|
||||
}, size);
|
||||
await pane.getByTestId("lobby_joinCall").click({ timeout: 60_000 });
|
||||
await expect(pane.locator("[data-layout]")).toBeVisible({ timeout: 60_000 });
|
||||
const component = await callShape(pane);
|
||||
await container.screenshot({
|
||||
path: test.info().outputPath("small-container.png"),
|
||||
});
|
||||
await referenceContext.close();
|
||||
|
||||
// The breakpoints in Element Call's stylesheets are container queries, so a
|
||||
// small container gets the compact footer a small window does, rather than
|
||||
// the full-width one the window's own size would call for
|
||||
expect(component).toEqual(reference);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
/* Hide everything but the subtitle in small windows */
|
||||
@media (max-height: 450px) {
|
||||
@container element-call (max-height: 450px) {
|
||||
.bar {
|
||||
display: none;
|
||||
}
|
||||
@@ -166,7 +166,7 @@
|
||||
}
|
||||
|
||||
/* Hide everything but the subtitle in small windows */
|
||||
@media (max-height: 450px) {
|
||||
@container element-call (max-height: 450px) {
|
||||
.bar:has(.subtitle) > header {
|
||||
grid-template-rows: var(--cpd-space-4x) minmax(var(--cpd-space-5x), auto);
|
||||
grid-template-areas: "." "subtitle";
|
||||
|
||||
@@ -107,7 +107,7 @@ Please see LICENSE in the repository root for full details.
|
||||
gap: var(--cpd-space-1-5x);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@container element-call (min-width: 800px) {
|
||||
.headerLogo,
|
||||
.leftNav.hideMobile,
|
||||
.rightNav.hideMobile {
|
||||
|
||||
+11
-1
@@ -33,7 +33,8 @@ the app and for the component build. */
|
||||
@import url("@fontsource/inconsolata/700.css");
|
||||
|
||||
@import url("normalize.css/normalize.css") layer(normalize);
|
||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
|
||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css")
|
||||
layer(compound);
|
||||
@import url("@vector-im/compound-web/dist/style.css") layer(compound.components);
|
||||
|
||||
:root {
|
||||
@@ -76,6 +77,15 @@ the app and for the component build. */
|
||||
--video-tile-background: var(--cpd-color-bg-subtle-secondary);
|
||||
}
|
||||
|
||||
/* The breakpoints in Element Call's stylesheets are container queries against
|
||||
this element rather than media queries against the viewport. For the standalone
|
||||
app the two are the same thing, since the root is the page; for a host that
|
||||
embeds Element Call in a corner of its own page they are not, and it is the
|
||||
corner that the layout has to fit. */
|
||||
[data-element-call-root] {
|
||||
container: element-call / size;
|
||||
}
|
||||
|
||||
.cpd-theme-dark {
|
||||
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
||||
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
@container element-call (max-width: 420px) {
|
||||
.reactionPopupMenu {
|
||||
--reaction-button-padding: 8px;
|
||||
--reaction-button-fontsize: 16px;
|
||||
|
||||
@@ -77,7 +77,7 @@ Please see LICENSE in the repository root for full details.
|
||||
}
|
||||
|
||||
/*First hide the logo*/
|
||||
@media (max-width: 750px) {
|
||||
@container element-call (max-width: 750px) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ Please see LICENSE in the repository root for full details.
|
||||
With the logo hidden >500px is enough space to show overflow, buttons, layout.
|
||||
Once we exceed 500 we hide everything except the buttons.
|
||||
*/
|
||||
@media (max-width: 500px) {
|
||||
@container element-call (max-width: 500px) {
|
||||
.footer {
|
||||
grid-template-areas: "buttons buttons buttons";
|
||||
}
|
||||
@@ -115,27 +115,27 @@ Once we exceed 500 we hide everything except the buttons.
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 800px) {
|
||||
@container element-call (max-height: 800px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-8x)
|
||||
calc(env(safe-area-inset-bottom) + var(--cpd-space-8x));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 400px) {
|
||||
@container element-call (max-height: 400px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-4x)
|
||||
calc(env(safe-area-inset-bottom) + var(--cpd-space-4x));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 370px) {
|
||||
@container element-call (max-width: 370px) {
|
||||
.shareScreen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* PIP custom css */
|
||||
@media (max-height: 400px) {
|
||||
@container element-call (max-height: 400px) {
|
||||
.shareScreen {
|
||||
display: flex;
|
||||
}
|
||||
@@ -148,13 +148,13 @@ Once we exceed 500 we hide everything except the buttons.
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
@container element-call (max-width: 320px) {
|
||||
.raiseHand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@container element-call (min-width: 800px) {
|
||||
.buttons {
|
||||
gap: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ Please see LICENSE in the repository root for full details.
|
||||
block-size: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@container element-call (max-width: 600px) {
|
||||
/* Give the PiP a portrait aspect ratio */
|
||||
.pip[data-size="sm"] {
|
||||
inline-size: 88px;
|
||||
|
||||
@@ -25,7 +25,7 @@ Please see LICENSE in the repository root for full details.
|
||||
var(--content-inset-left);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
@container element-call (min-width: 600px) {
|
||||
.pip {
|
||||
inline-size: 180px;
|
||||
block-size: 135px;
|
||||
|
||||
@@ -74,7 +74,7 @@ Please see LICENSE in the repository root for full details.
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@container element-call (min-width: 800px) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ spotlight tile is maximised and displaying video, apply a gradient background. *
|
||||
background: none;
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
@container element-call (max-width: 320px) {
|
||||
.invite {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ Please see LICENSE in the repository root for full details.
|
||||
color: var(--cpd-color-theme-primary) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
@container element-call (max-width: 500px) {
|
||||
.join {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 650px) {
|
||||
@container element-call (min-height: 650px) {
|
||||
.content {
|
||||
gap: var(--cpd-space-10x);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ video.mirror {
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
@container element-call (max-width: 550px) {
|
||||
.preview {
|
||||
margin-inline: 0;
|
||||
border-radius: 0;
|
||||
|
||||
Reference in New Issue
Block a user