mirror of
https://github.com/element-hq/element-call.git
synced 2026-10-09 14:07:26 +00:00
- Compound 10.3.0 bounds a menu by the space it has and scrolls what doesn't fit, within a boundary the app passes. The menus pass the call area, 16px in from its sides, rather than measuring its width. - The 296px width is the menu's own style. Compound gives that style only to the dropdown, so menuIsDrawer, which copied its phone rule, goes. - Compound draws the menu's frame over its content, so nothing that scrolls can hide it in Safari: the list, headings and meter no longer keep a border width clear of it, and the checks that they did go. - The device list is Compound's scroll area, so it fades out at the foot with Compound's chevron while there is more below. - The microphone menu's meter sticks at that foot, with rows scrolling under it as before, so the list has no fade above it: Compound's is a mask fixed to the list's edge, while a bounce at the top carries the meter with the rows. The band under the meter is filled, or rows would show there. The edge fades from where the chevron starts, so a bounce fades the meter out before it reaches the chevron. A row reached by keyboard stays above the meter. - The list's height is still its share of the call. - The component harness leaves the host's stylesheets unscoped, Compound's among them, as a host does: scoped, they never reached the menu, which is portalled out of Element Call's root. - Its specs check only Element Call's own document styles on the host page, and find the device list within Compound's scroll area. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import postcss from "postcss";
|
|
|
|
import { ROOT_SELECTOR, scopeStylesToRoot } from "./scopeStylesToRoot";
|
|
|
|
const inRoot = `:where(${ROOT_SELECTOR}, ${ROOT_SELECTOR} *)`;
|
|
const isRoot = `:where(${ROOT_SELECTOR})`;
|
|
|
|
async function scope(css: string, file = "base.css"): Promise<string> {
|
|
const result = await postcss([scopeStylesToRoot()]).process(css, {
|
|
from: file,
|
|
});
|
|
return result.css;
|
|
}
|
|
|
|
describe("scopeStylesToRoot", () => {
|
|
it("makes the root stand in for the document", async () => {
|
|
expect(await scope("html { line-height: 1.15 }")).toBe(
|
|
`${isRoot} { line-height: 1.15 }`,
|
|
);
|
|
expect(await scope("body { margin: 0 }")).toBe(`${isRoot} { margin: 0 }`);
|
|
expect(await scope(":root { --a: 1 }")).toBe(`${isRoot} { --a: 1 }`);
|
|
expect(await scope("body.no-scroll-body { position: fixed }")).toBe(
|
|
`${isRoot}.no-scroll-body { position: fixed }`,
|
|
);
|
|
expect(await scope("body .x { color: red }")).toBe(
|
|
`${isRoot} .x { color: red }`,
|
|
);
|
|
});
|
|
|
|
it("collapses selectors that all became the root", async () => {
|
|
expect(await scope("html, body, input { font: inherit }")).toBe(
|
|
`${isRoot},input${inRoot} { font: inherit }`,
|
|
);
|
|
});
|
|
|
|
it("confines everything else to the root and what is inside it", async () => {
|
|
expect(await scope("h1 { margin: 0 }")).toBe(`h1${inRoot} { margin: 0 }`);
|
|
expect(await scope(".cpd-theme-dark { --a: 1 }")).toBe(
|
|
`.cpd-theme-dark${inRoot} { --a: 1 }`,
|
|
);
|
|
expect(await scope("* { box-sizing: border-box }")).toBe(
|
|
`*${inRoot} { box-sizing: border-box }`,
|
|
);
|
|
expect(await scope(".a > .b + .c { color: red }")).toBe(
|
|
`.a>.b+.c${inRoot} { color: red }`,
|
|
);
|
|
});
|
|
|
|
it("keeps pseudo-elements last", async () => {
|
|
expect(await scope("button::-moz-focus-inner { border: 0 }")).toBe(
|
|
`button${inRoot}::-moz-focus-inner { border: 0 }`,
|
|
);
|
|
expect(await scope(".a .b:hover::after { content: '' }")).toBe(
|
|
`.a .b:hover${inRoot}::after { content: '' }`,
|
|
);
|
|
expect(await scope("p:first-letter { color: red }")).toBe(
|
|
`p${inRoot}:first-letter { color: red }`,
|
|
);
|
|
});
|
|
|
|
it("leaves alone what already names the root", async () => {
|
|
const css = `${ROOT_SELECTOR}[data-platform="ios"] { --a: 1 }`;
|
|
expect(await scope(css)).toBe(css);
|
|
});
|
|
|
|
it("reaches into layers and media queries", async () => {
|
|
expect(
|
|
await scope(
|
|
"@layer normalize { h1 { margin: 0 } } @media (min-width: 1px) { p { margin: 0 } }",
|
|
),
|
|
).toBe(
|
|
`@layer normalize { h1${inRoot} { margin: 0 } } @media (min-width: 1px) { p${inRoot} { margin: 0 } }`,
|
|
);
|
|
});
|
|
|
|
it("does not touch keyframes or nested rules", async () => {
|
|
expect(
|
|
await scope("@keyframes spin { from { opacity: 0 } to { opacity: 1 } }"),
|
|
).toBe("@keyframes spin { from { opacity: 0 } to { opacity: 1 } }");
|
|
expect(
|
|
await scope(
|
|
".a { color: red; &:hover { color: blue } .b { color: green } }",
|
|
),
|
|
).toBe(
|
|
`.a${inRoot} { color: red; &:hover { color: blue } .b { color: green } }`,
|
|
);
|
|
});
|
|
|
|
it("only touches the bare selectors of a CSS module", async () => {
|
|
const file = "Settings.module.css";
|
|
expect(await scope("pre { font-size: 1px }", file)).toBe(
|
|
`pre${inRoot} { font-size: 1px }`,
|
|
);
|
|
expect(await scope(".modal pre { font-size: 1px }", file)).toBe(
|
|
`.modal pre${inRoot} { font-size: 1px }`,
|
|
);
|
|
expect(await scope(".box_abc12 { border: 0 }", file)).toBe(
|
|
".box_abc12 { border: 0 }",
|
|
);
|
|
expect(await scope(".a .b_abc12:hover { border: 0 }", file)).toBe(
|
|
".a .b_abc12:hover { border: 0 }",
|
|
);
|
|
});
|
|
|
|
it("leaves an excluded stylesheet as it is", async () => {
|
|
const css = "body { margin: 0 } .menu { color: red }";
|
|
const result = await postcss([
|
|
scopeStylesToRoot({ exclude: [/host\.css$/] }),
|
|
]).process(css, { from: "/app/host.css" });
|
|
expect(result.css).toBe(css);
|
|
});
|
|
});
|