mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
remove scopeStylesToRoot
This commit is contained in:
@@ -252,11 +252,10 @@ for the standalone app the root is the page, so they mean what the media queries
|
|||||||
they replaced did. (The standalone-only views, such as the home and login pages,
|
they replaced did. (The standalone-only views, such as the home and login pages,
|
||||||
still use plain media queries, since the component never shows them.)
|
still use plain media queries, since the component never shows them.)
|
||||||
|
|
||||||
The component's stylesheet is confined to the element it is mounted in: the
|
The component's stylesheet is confined to the element it is mounted in: its
|
||||||
build rewrites every selector so that it matches only Element Call's root or
|
base styles — element defaults and custom properties — are written against
|
||||||
what is inside it, with `html`, `body` and `:root` standing for that root (see
|
Element Call's root element rather than the document, and everything else is a
|
||||||
`component/build/scopeStylesToRoot.ts`). A host's own page keeps its styles,
|
CSS module. A host's own page keeps its styles.
|
||||||
and Element Call brings its own fonts and design tokens along.
|
|
||||||
|
|
||||||
The component speaks every language the app does. English is bundled in; the
|
The component speaks every language the app does. English is bundled in; the
|
||||||
other locales are split into chunks the host's bundler loads the first time
|
other locales are split into chunks the host's bundler loads the first time
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
/*
|
|
||||||
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 }",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -82,8 +82,9 @@ its caller? If not, cut it.
|
|||||||
- Size against the root, not the window: `@container element-call (…)` and
|
- Size against the root, not the window: `@container element-call (…)` and
|
||||||
`cqw` / `cqh`, never `@media (width)` or `vw` / `vh`. Media queries stay correct
|
`cqw` / `cqh`, never `@media (width)` or `vw` / `vh`. Media queries stay correct
|
||||||
only in standalone-only views — home, login.
|
only in standalone-only views — home, login.
|
||||||
- Style `[data-element-call-root]`, never `body` or `:root`; the component build
|
- Style `[data-element-call-root]`, never `body` or `:root`. A rule that matches
|
||||||
makes those stand for the root (`component/build/scopeStylesToRoot.ts`).
|
by element alone takes the root as a prefix (see `src/base.css`); nothing
|
||||||
|
rewrites selectors at build time, so a bare one reaches the host's page.
|
||||||
|
|
||||||
## Strings and a11y
|
## Strings and a11y
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ export default {
|
|||||||
"src/state/ServiceInterruptionsViewModel.ts",
|
"src/state/ServiceInterruptionsViewModel.ts",
|
||||||
],
|
],
|
||||||
ignoreDependencies: [
|
ignoreDependencies: [
|
||||||
// Used in CSS
|
|
||||||
"normalize.css",
|
|
||||||
// Used for its global type declarations
|
// Used for its global type declarations
|
||||||
"@types/grecaptcha",
|
"@types/grecaptcha",
|
||||||
"@types/sdp-transform",
|
"@types/sdp-transform",
|
||||||
|
|||||||
@@ -106,14 +106,12 @@
|
|||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^1.18.0",
|
"matrix-widget-api": "^1.18.0",
|
||||||
"node-stdlib-browser": "^1.3.1",
|
"node-stdlib-browser": "^1.3.1",
|
||||||
"normalize.css": "^8.0.1",
|
|
||||||
"oxfmt": "^0.66.0",
|
"oxfmt": "^0.66.0",
|
||||||
"oxlint": "^1.70.0",
|
"oxlint": "^1.70.0",
|
||||||
"oxlint-tsgolint": "^0.25.0",
|
"oxlint-tsgolint": "^0.25.0",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-selector-parser": "^7.1.1",
|
|
||||||
"posthog-js": "1.408.2",
|
"posthog-js": "1.408.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "19",
|
"react": "19",
|
||||||
|
|||||||
Generated
-11
@@ -199,9 +199,6 @@ importers:
|
|||||||
node-stdlib-browser:
|
node-stdlib-browser:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1
|
version: 1.3.1
|
||||||
normalize.css:
|
|
||||||
specifier: ^8.0.1
|
|
||||||
version: 8.0.1
|
|
||||||
oxfmt:
|
oxfmt:
|
||||||
specifier: ^0.66.0
|
specifier: ^0.66.0
|
||||||
version: 0.66.0
|
version: 0.66.0
|
||||||
@@ -220,9 +217,6 @@ importers:
|
|||||||
postcss-preset-env:
|
postcss-preset-env:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.6.1(postcss@8.5.26)
|
version: 10.6.1(postcss@8.5.26)
|
||||||
postcss-selector-parser:
|
|
||||||
specifier: ^7.1.1
|
|
||||||
version: 7.1.3
|
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: 1.408.2
|
specifier: 1.408.2
|
||||||
version: 1.408.2
|
version: 1.408.2
|
||||||
@@ -4784,9 +4778,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
normalize.css@8.0.1:
|
|
||||||
resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==}
|
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
npm-run-path@6.0.0:
|
||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -10369,8 +10360,6 @@ snapshots:
|
|||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize.css@8.0.1: {}
|
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
npm-run-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 4.0.0
|
path-key: 4.0.0
|
||||||
|
|||||||
+40
-43
@@ -10,14 +10,22 @@ and element defaults its own stylesheets build on top of.
|
|||||||
|
|
||||||
Split out from index.css so that Element Call as a component can have these
|
Split out from index.css so that Element Call as a component can have these
|
||||||
without also being given the standalone page's layout, which would style the
|
without also being given the standalone page's layout, which would style the
|
||||||
host's own document. What remains here still speaks of the
|
host's own document. Compound's stylesheets — the design tokens, the component
|
||||||
document — normalize.css and the typography below use bare element selectors,
|
styles and the fonts they use — are not here either: as a component, Element
|
||||||
and the custom properties are declared on `:root` — which is right for the
|
Call shares the host's copy of Compound (it is a peer dependency, so that what
|
||||||
page, and is why the component build rewrites it: there every selector is
|
Compound floats into the host's body, tooltips and menus, is styled by the
|
||||||
confined to Element Call's root element (see component/build/scopeStylesToRoot.ts),
|
host's stylesheet), and index.css loads it for the page.
|
||||||
with `html`, `body` and `:root` becoming that element. Nothing needs to be
|
|
||||||
written differently here for that to work, but nothing here may rely on
|
Everything here is written against Element Call's root element, found by the
|
||||||
reaching the host's document either.
|
`data-element-call-root` attribute useTheme puts on it: the custom properties
|
||||||
|
are declared on it rather than on `:root`, and the element defaults — the
|
||||||
|
normalize.css next to this file, and the typography below — take it as a prefix
|
||||||
|
so that they reach only what is inside it. For the standalone page the root is
|
||||||
|
the body, so this means what the unscoped rules did; for a component it is the
|
||||||
|
container the host gave, and the host's document is left as it was. Nothing
|
||||||
|
rewrites selectors at build time, so a bare `h1` or `.theme` here would reach
|
||||||
|
the host's page in every build. CSS modules need none of this, since their
|
||||||
|
class names already confine them.
|
||||||
|
|
||||||
Nothing here should depend on where it lands relative to Element Call's
|
Nothing here should depend on where it lands relative to Element Call's
|
||||||
component stylesheets: the bundler decides that, and it decides differently for
|
component stylesheets: the bundler decides that, and it decides differently for
|
||||||
@@ -32,11 +40,11 @@ the app and for the component build. */
|
|||||||
@import url("@fontsource/inconsolata/400.css");
|
@import url("@fontsource/inconsolata/400.css");
|
||||||
@import url("@fontsource/inconsolata/700.css");
|
@import url("@fontsource/inconsolata/700.css");
|
||||||
|
|
||||||
@import url("normalize.css/normalize.css") layer(normalize);
|
@import url("./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);
|
@import url("@vector-im/compound-web/dist/style.css") layer(compound.components);
|
||||||
|
|
||||||
:root {
|
[data-element-call-root] {
|
||||||
--font-scale: 1;
|
--font-scale: 1;
|
||||||
--font-size-micro: calc(10px * var(--font-scale));
|
--font-size-micro: calc(10px * var(--font-scale));
|
||||||
--font-size-caption: calc(12px * var(--font-scale));
|
--font-size-caption: calc(12px * var(--font-scale));
|
||||||
@@ -73,8 +81,8 @@ the app and for the component build. */
|
|||||||
--call-view-header-footer-layer: 2;
|
--call-view-header-footer-layer: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root,
|
[data-element-call-root],
|
||||||
[class*="cpd-theme-"] {
|
[data-element-call-root] [class*="cpd-theme-"] {
|
||||||
--video-tile-background: var(--cpd-color-bg-subtle-secondary);
|
--video-tile-background: var(--cpd-color-bg-subtle-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +101,8 @@ before using one further down the tree. */
|
|||||||
container: element-call / size;
|
container: element-call / size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpd-theme-dark {
|
[data-element-call-root].cpd-theme-dark,
|
||||||
|
[data-element-call-root] .cpd-theme-dark {
|
||||||
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
||||||
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
||||||
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
||||||
@@ -120,11 +129,6 @@ before using one further down the tree. */
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We use this to not render the page at all until we know the theme.*/
|
|
||||||
.no-theme {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On Android and iOS, prefer native system fonts. The global.css file of
|
/* On Android and iOS, prefer native system fonts. The global.css file of
|
||||||
Compound Web is where these variables ultimately get consumed to set the page's
|
Compound Web is where these variables ultimately get consumed to set the page's
|
||||||
font-family. */
|
font-family. */
|
||||||
@@ -138,54 +142,45 @@ font-family. */
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer compound-legacy {
|
@layer compound-legacy {
|
||||||
h1,
|
[data-element-call-root] :is(h1, h2, h3, h4, h5, h6, p, a) {
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
a {
|
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Headline Semi Bold */
|
/* Headline Semi Bold */
|
||||||
h1 {
|
[data-element-call-root] h1 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-headline);
|
font-size: var(--font-size-headline);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Title */
|
/* Title */
|
||||||
h2 {
|
[data-element-call-root] h2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-title);
|
font-size: var(--font-size-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtitle */
|
/* Subtitle */
|
||||||
h3 {
|
[data-element-call-root] h3 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-subtitle);
|
font-size: var(--font-size-subtitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body Semi Bold */
|
/* Body Semi Bold */
|
||||||
h4 {
|
[data-element-call-root] h4 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-body);
|
font-size: var(--font-size-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
[data-element-call-root] :is(h1, h2, h3) {
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
p {
|
[data-element-call-root] p {
|
||||||
font-size: var(--font-size-body);
|
font-size: var(--font-size-body);
|
||||||
line-height: var(--font-size-title);
|
line-height: var(--font-size-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
[data-element-call-root] hr {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
|
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||||
@@ -199,25 +194,27 @@ font-family. */
|
|||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
[data-element-call-root] summary {
|
||||||
font-size: var(--font-size-body);
|
font-size: var(--font-size-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
details > :not(summary) {
|
[data-element-call-root] details > :not(summary) {
|
||||||
margin-left: var(--font-size-body);
|
margin-left: var(--font-size-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] > summary {
|
[data-element-call-root] details[open] > summary {
|
||||||
margin-bottom: var(--font-size-body);
|
margin-bottom: var(--font-size-body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom
|
/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom
|
||||||
outline, which is inconsistent with our other components and is not sufficiently
|
outline, which is inconsistent with our other components and is not sufficiently
|
||||||
visible to be accessible. This resets it back to 'auto'. */
|
visible to be accessible. This resets it back to 'auto'. The root is named in
|
||||||
button:-moz-focusring,
|
`:where()`, which adds no specificity, so that the focus styles components set
|
||||||
[type="button"]:-moz-focusring,
|
with a class still win over this. */
|
||||||
[type="reset"]:-moz-focusring,
|
:where([data-element-call-root]) button:-moz-focusring,
|
||||||
[type="submit"]:-moz-focusring {
|
:where([data-element-call-root]) [type="button"]:-moz-focusring,
|
||||||
|
:where([data-element-call-root]) [type="reset"]:-moz-focusring,
|
||||||
|
:where([data-element-call-root]) [type="submit"]:-moz-focusring {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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 { readFile } from "node:fs/promises";
|
||||||
|
import postcss, { type AtRule, type Rule } from "postcss";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
// As a component, Element Call's stylesheets share a document with the host's.
|
||||||
|
// CSS modules keep to themselves through their class names, but the base styles
|
||||||
|
// speak of bare elements and custom properties, and nothing rewrites them at
|
||||||
|
// build time: each rule has to name the root itself.
|
||||||
|
const ROOT = "[data-element-call-root]";
|
||||||
|
|
||||||
|
/** Whether a rule is relative to something other than the document. */
|
||||||
|
function isRelative(rule: Rule): boolean {
|
||||||
|
for (let parent = rule.parent; parent; parent = parent.parent) {
|
||||||
|
if (parent.type === "rule") return true;
|
||||||
|
if (parent.type === "atrule") {
|
||||||
|
const { name } = parent as AtRule;
|
||||||
|
if (name.endsWith("keyframes") || name === "page") return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each(["src/base.css", "src/normalize.css"])("%s", (file) => {
|
||||||
|
it("confines every rule to Element Call's root", async () => {
|
||||||
|
const root = postcss.parse(await readFile(file, "utf8"), { from: file });
|
||||||
|
const unscoped: string[] = [];
|
||||||
|
root.walkRules((rule) => {
|
||||||
|
if (isRelative(rule)) return;
|
||||||
|
for (const selector of rule.selectors)
|
||||||
|
if (!selector.includes(ROOT))
|
||||||
|
unscoped.push(`${rule.source?.start?.line}: ${selector}`);
|
||||||
|
});
|
||||||
|
expect(unscoped).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,6 +31,13 @@ body.no-scroll-body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* index.html starts the body with this class, so that nothing is painted until
|
||||||
|
useTheme has decided the theme and removed it. Only the page starts out hidden
|
||||||
|
like this; a host's container is shown whenever the host shows it. */
|
||||||
|
body.no-theme {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
|
|||||||
Vendored
+354
@@ -0,0 +1,354 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* normalize.css, written against Element Call's root element instead of the
|
||||||
|
document. Upstream's rules are kept word for word, with `html` and `body`
|
||||||
|
replaced by `[data-element-call-root]` and every other selector prefixed with
|
||||||
|
it, so that as a component Element Call normalizes only what is inside the
|
||||||
|
container its host gave it. For the standalone page that container is the
|
||||||
|
body, so this means what the unscoped stylesheet did. Upstream's `body`
|
||||||
|
margin is left out: the page's margin is index.css's business, and a host's
|
||||||
|
container is not ours to reset.
|
||||||
|
|
||||||
|
Diff against upstream's normalize.css to see the changes. */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] b,
|
||||||
|
[data-element-call-root] strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] code,
|
||||||
|
[data-element-call-root] kbd,
|
||||||
|
[data-element-call-root] samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] sub,
|
||||||
|
[data-element-call-root] sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-element-call-root] sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-element-call-root] sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button,
|
||||||
|
[data-element-call-root] input,
|
||||||
|
[data-element-call-root] optgroup,
|
||||||
|
[data-element-call-root] select,
|
||||||
|
[data-element-call-root] textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button,
|
||||||
|
[data-element-call-root] input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button,
|
||||||
|
[data-element-call-root] select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button,
|
||||||
|
[data-element-call-root] [type="button"],
|
||||||
|
[data-element-call-root] [type="reset"],
|
||||||
|
[data-element-call-root] [type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button::-moz-focus-inner,
|
||||||
|
[data-element-call-root] [type="button"]::-moz-focus-inner,
|
||||||
|
[data-element-call-root] [type="reset"]::-moz-focus-inner,
|
||||||
|
[data-element-call-root] [type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] button:-moz-focusring,
|
||||||
|
[data-element-call-root] [type="button"]:-moz-focusring,
|
||||||
|
[data-element-call-root] [type="reset"]:-moz-focusring,
|
||||||
|
[data-element-call-root] [type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] [type="checkbox"],
|
||||||
|
[data-element-call-root] [type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] [type="number"]::-webkit-inner-spin-button,
|
||||||
|
[data-element-call-root] [type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] [type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] [type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] ::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-element-call-root] [hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import { realpathSync } from "node:fs";
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
import { vitePluginsConfig } from "./vite.config.ts";
|
import { vitePluginsConfig } from "./vite.config.ts";
|
||||||
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
|
||||||
|
|
||||||
// Serves the harness under `component/dev`, which embeds Element Call as a
|
// Serves the harness under `component/dev`, which embeds Element Call as a
|
||||||
// component the way a host application would. Development only: this is not
|
// component the way a host application would. Development only: this is not
|
||||||
@@ -36,9 +35,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
return {
|
return {
|
||||||
...vitePluginsConfig({ mode, html: false }),
|
...vitePluginsConfig({ mode, html: false }),
|
||||||
root: "component/dev",
|
root: "component/dev",
|
||||||
// The same scoping the library build applies, so the harness shows what a
|
|
||||||
// host will get — including whether its own page is left alone
|
|
||||||
css: { postcss: { plugins: [scopeStylesToRoot()] } },
|
|
||||||
// So that the harness can read the same config.json the standalone app
|
// So that the harness can read the same config.json the standalone app
|
||||||
// does, if the developer has written one
|
// does, if the developer has written one
|
||||||
publicDir: "../../public",
|
publicDir: "../../public",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
import { vitePluginsConfig } from "./vite.config.ts";
|
import { vitePluginsConfig } from "./vite.config.ts";
|
||||||
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
|
||||||
|
|
||||||
// Config for Element Call as a React component, to be imported by an
|
// Config for Element Call as a React component, to be imported by an
|
||||||
// application embedding it rather than served as a page of its own.
|
// application embedding it rather than served as a page of its own.
|
||||||
@@ -34,9 +33,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
// whatever is in `public` — including the developer's own config.json, which
|
// whatever is in `public` — including the developer's own config.json, which
|
||||||
// is not in the repository — into the output we would publish.
|
// is not in the repository — into the output we would publish.
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
// A host's document is not ours to style: everything in the stylesheet is
|
|
||||||
// confined to the element Element Call is mounted in
|
|
||||||
css: { postcss: { plugins: [scopeStylesToRoot()] } },
|
|
||||||
build: {
|
build: {
|
||||||
// Into the package directory, so that `component/package.json` describes
|
// Into the package directory, so that `component/package.json` describes
|
||||||
// what sits next to it and the directory can be installed as a package
|
// what sits next to it and the directory can be installed as a package
|
||||||
|
|||||||
Reference in New Issue
Block a user