mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-10 23:25:52 +00:00
Merge pull request #4306 from element-hq/toger5/fix-component-tooltips
[ElementCall Component] Make compound a peer dependency with EW. (fix tooltips in embedded mode)
This commit is contained in:
@@ -297,8 +297,8 @@ The package is not published yet. A host installs it as a git dependency on the
|
||||
```
|
||||
|
||||
whose `prepare` script runs the build on install. That build needs pnpm (via
|
||||
Corepack) on the host's machine, runs a full `pnpm install` of this repository
|
||||
and is memory-hungry, since it inherits the `--max-old-space-size` setting of
|
||||
Corepack) on the host's machine, runs a full `pnpm install` of this repository,
|
||||
since it inherits the `--max-old-space-size` setting of
|
||||
the app build; the host's pnpm also has to allow it to run at all
|
||||
(`allowBuilds` in its `pnpm-workspace.yaml`). Note that `component/` is a pnpm
|
||||
project of its own for this reason, so pnpm commands run from inside that
|
||||
@@ -306,8 +306,10 @@ directory target it rather than the repository; run them from the repository
|
||||
root. The host imports the component from
|
||||
`@element-hq/element-call-component` and the stylesheet from
|
||||
`@element-hq/element-call-component/style.css`, and has to provide `react`,
|
||||
`react-dom`, `matrix-js-sdk` and `livekit-client` itself, since the bundle leaves
|
||||
them external.
|
||||
`react-dom`, `matrix-js-sdk`, `livekit-client`, `@vector-im/compound-web` and
|
||||
`@vector-im/compound-design-tokens` itself, since the bundle leaves them
|
||||
external. Compound's stylesheet are the host's to load too: the component shares
|
||||
the host's copy of Compound, (required for example for tooltips)
|
||||
|
||||
The component is large, and a host will usually load it lazily, only once a
|
||||
call is shown. Everything a host needs in order to talk about a call before
|
||||
|
||||
@@ -34,10 +34,18 @@ component rather than a page makes. */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Compound's overlay container, which holds tooltips and popovers, has to fill
|
||||
the container for the elements inside it to be positioned against it. The
|
||||
standalone page does the same for the container under `#root`. */
|
||||
.root > [data-overlay-container] {
|
||||
position: relative;
|
||||
/* The call itself. It is a stacking context of its own, as the standalone
|
||||
page's `#root` is, so that the layers the call view uses for its header, footer
|
||||
and overlays stay below the modals and toasts, which are portalled into the
|
||||
root and carry no layer of their own.
|
||||
|
||||
What Compound floats — tooltips, menus — is not in here at all: it goes into
|
||||
the host's body, as it would for any other component on the host's page, and is
|
||||
styled and layered by the host's copy of Compound. */
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@ const IS_ROOT = `:where(${ROOT_SELECTOR})`;
|
||||
* Element Call as a component.
|
||||
*
|
||||
* As a page of its own, Element Call can style the document: normalize.css and
|
||||
* Compound speak of `html`, `body` and bare elements, and the design tokens are
|
||||
* declared on `:root`. As a component, all of that would land on the host's
|
||||
* document too. This rewrites every selector so that it matches only the root
|
||||
* or its descendants:
|
||||
* its own base styles speak of `html`, `body` and bare elements, and its custom
|
||||
* properties are declared on `:root`. As a component, all of that would land on
|
||||
* the host's document too. This rewrites every selector so that it matches only
|
||||
* the root or its descendants:
|
||||
*
|
||||
* - `html`, `body` and `:root` become the root element, which is what stands in
|
||||
* for the document inside a host.
|
||||
@@ -55,9 +55,10 @@ const IS_ROOT = `:where(${ROOT_SELECTOR})`;
|
||||
* CSS modules are scoped by their class names already, so only their selectors
|
||||
* that would match by element alone — `pre` rather than `.pre` — are touched.
|
||||
*
|
||||
* The root's fonts and design tokens are still inherited by everything inside
|
||||
* it, the way they were from `body` and `:root`, and `@font-face` declarations
|
||||
* stay global, which they are by nature.
|
||||
* The root's custom properties are still inherited by everything inside it,
|
||||
* the way they were from `:root`. Compound's own stylesheet — the design
|
||||
* tokens, the component styles, the fonts — is not in this build at all: the
|
||||
* host supplies it, unscoped, along with its copy of Compound.
|
||||
*/
|
||||
export function scopeStylesToRoot(): Plugin {
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,16 @@ import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
// What a host provides along with its copy of Compound: its stylesheet, the
|
||||
// design tokens and the fonts they name. The component does not bundle them.
|
||||
import "@fontsource/inter/400.css";
|
||||
import "@fontsource/inter/500.css";
|
||||
import "@fontsource/inter/600.css";
|
||||
import "@fontsource/inter/700.css";
|
||||
import "@fontsource/inconsolata/400.css";
|
||||
import "@fontsource/inconsolata/700.css";
|
||||
import "@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css";
|
||||
import "@vector-im/compound-web/dist/style.css";
|
||||
import { type ConfigOptions, initializeElementCall } from "../index";
|
||||
import { Harness } from "./Harness";
|
||||
// After Element Call's, so that the host has the last word on its own page
|
||||
|
||||
+40
-30
@@ -17,19 +17,25 @@ Please see LICENSE in the repository root for full details.
|
||||
* host instead, or is confined to the container it is mounted in.
|
||||
*/
|
||||
|
||||
// The design tokens, fonts and element defaults every Element Call stylesheet
|
||||
// The element defaults and custom properties every Element Call stylesheet
|
||||
// builds on. Written for a page, they speak of `html`, `body` and bare
|
||||
// elements; the component build confines them, and every other stylesheet in
|
||||
// this bundle, to the root element below (see build/scopeStylesToRoot.ts), so
|
||||
// that the host's document is left as it was.
|
||||
//
|
||||
// Compound is not in here. The component uses the host's copy of it (a peer
|
||||
// dependency, left external by the build), and the host loads Compound's
|
||||
// stylesheet, design tokens and fonts along with it: what Compound floats into
|
||||
// the host's body — tooltips, menus — is then styled by the same stylesheet as
|
||||
// everything else there, and Element Call's root takes the tokens and fonts of
|
||||
// the theme class it carries, as any element on the host's page would.
|
||||
//
|
||||
// Where these land relative to the component stylesheets is the bundler's
|
||||
// choice — the standalone app puts them first, this build puts them in the
|
||||
// middle — so nothing in base.css may depend on winning or losing against a
|
||||
// component's own rules at equal specificity. It currently does not: what it
|
||||
// declares unlayered is custom properties on Element Call's root, which
|
||||
// components inherit rather than compete with, and everything from Compound
|
||||
// sits in a `@layer`, which loses to unlayered rules either way.
|
||||
// components inherit rather than compete with.
|
||||
import "../src/base.css";
|
||||
|
||||
import {
|
||||
@@ -231,34 +237,38 @@ export const ElementCall: FC<ElementCallProps> = ({
|
||||
<UrlParamsProvider value={params}>
|
||||
<div ref={setContainer} className={styles.root}>
|
||||
{ready && (
|
||||
<RootElementProvider value={container}>
|
||||
{/* Whatever goes wrong in here is shown in here. Left to
|
||||
// Stands in for the standalone page's `#root`: see `.content`
|
||||
// in ElementCall.module.css
|
||||
<div className={styles.content}>
|
||||
<RootElementProvider value={container}>
|
||||
{/* Whatever goes wrong in here is shown in here. Left to
|
||||
propagate, an error would unmount the host's own tree. */}
|
||||
<ErrorBoundary
|
||||
fallback={(error) => <ErrorPage error={error} />}
|
||||
// A broken call should not hold the host on screen
|
||||
onError={() => void hostBridge.setAlwaysOnScreen(false)}
|
||||
>
|
||||
<Decoration>
|
||||
<TooltipProvider>
|
||||
<ClientProvider client={client}>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<ProcessorProvider>
|
||||
<CallView
|
||||
client={client}
|
||||
rtcSession={rtcSession}
|
||||
isPasswordlessUser={false}
|
||||
confineToRoom={params.confineToRoom}
|
||||
preload={params.preload}
|
||||
skipLobby={params.skipLobby}
|
||||
/>
|
||||
</ProcessorProvider>
|
||||
</MediaDevicesContext>
|
||||
</ClientProvider>
|
||||
</TooltipProvider>
|
||||
</Decoration>
|
||||
</ErrorBoundary>
|
||||
</RootElementProvider>
|
||||
<ErrorBoundary
|
||||
fallback={(error) => <ErrorPage error={error} />}
|
||||
// A broken call should not hold the host on screen
|
||||
onError={() => void hostBridge.setAlwaysOnScreen(false)}
|
||||
>
|
||||
<Decoration>
|
||||
<TooltipProvider>
|
||||
<ClientProvider client={client}>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<ProcessorProvider>
|
||||
<CallView
|
||||
client={client}
|
||||
rtcSession={rtcSession}
|
||||
isPasswordlessUser={false}
|
||||
confineToRoom={params.confineToRoom}
|
||||
preload={params.preload}
|
||||
skipLobby={params.skipLobby}
|
||||
/>
|
||||
</ProcessorProvider>
|
||||
</MediaDevicesContext>
|
||||
</ClientProvider>
|
||||
</TooltipProvider>
|
||||
</Decoration>
|
||||
</ErrorBoundary>
|
||||
</RootElementProvider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</UrlParamsProvider>
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
"prepare": "cd .. && pnpm install --frozen-lockfile && pnpm build:component"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vector-im/compound-design-tokens": "^11.0.0",
|
||||
"@vector-im/compound-web": "^10.0.0",
|
||||
"livekit-client": "^2.18.1",
|
||||
"matrix-js-sdk": "*",
|
||||
"react": "^19",
|
||||
|
||||
@@ -76,7 +76,10 @@ Code that builds in only one is a bug.
|
||||
- `build:component` — `@element-hq/element-call-component`, sources in `component/`
|
||||
(its own pnpm project; run pnpm from the repo root). Host API is in the README;
|
||||
`pnpm lint:externals` rejects an import of a `react` / `react-dom` /
|
||||
`matrix-js-sdk` / `livekit-client` subpath the externals list omits. Two entry
|
||||
`matrix-js-sdk` / `livekit-client` / `@vector-im/compound-web` /
|
||||
`@vector-im/compound-design-tokens` subpath the externals list omits.
|
||||
Compound's stylesheets are loaded by `src/index.css` for the page and by the
|
||||
host for the component, never by `src/base.css`. Two entry
|
||||
points: `index.tsx` (the component) and `api.ts` (types, enums and
|
||||
`configurationForIntent`, for a host that must not load the component). `api.ts`
|
||||
may only reach types and constants — `src/UrlConfiguration.ts`, not `UrlParams`.
|
||||
|
||||
@@ -9,9 +9,11 @@ Please see LICENSE in the repository root for full details.
|
||||
* Checks that the component build leaves the packages a host must supply to
|
||||
* the host.
|
||||
*
|
||||
* A host application already has React, the Matrix SDK and LiveKit, and a
|
||||
* second copy of any of them is worse than dead weight: React would hold two
|
||||
* sets of hooks, and the Matrix client would run two sync loops. So the
|
||||
* A host application already has React, the Matrix SDK, LiveKit and Compound,
|
||||
* and a second copy of any of them is worse than dead weight: React would hold
|
||||
* two sets of hooks, the Matrix client would run two sync loops, and a second
|
||||
* Compound would style the tooltips it floats into the host's body with class
|
||||
* names the host's stylesheet does not know. So the
|
||||
* component build lists them as external — but that list has to name every
|
||||
* subpath, since the bundler silently ignores the pattern and callback forms
|
||||
* of the option, and an import it does not cover is bundled with no warning at
|
||||
@@ -33,6 +35,9 @@ import { loadConfigFromFile } from "vite";
|
||||
|
||||
const CONFIG = "vite-component.config.ts";
|
||||
const SOURCES = ["src", "component"];
|
||||
// The development harness is a host, not part of the component: it is the
|
||||
// one that imports what a host supplies (Compound's stylesheets, say).
|
||||
const EXCLUDED = ["component/dev"];
|
||||
|
||||
/** The packages whose duplication would break a host, rather than merely enlarge it. */
|
||||
const MUST_BE_EXTERNAL = [
|
||||
@@ -40,6 +45,8 @@ const MUST_BE_EXTERNAL = [
|
||||
"react-dom",
|
||||
"matrix-js-sdk",
|
||||
"livekit-client",
|
||||
"@vector-im/compound-web",
|
||||
"@vector-im/compound-design-tokens",
|
||||
];
|
||||
|
||||
const isTestFile = (name) =>
|
||||
@@ -49,6 +56,7 @@ const isTestFile = (name) =>
|
||||
async function* sourceFiles(dir) {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (EXCLUDED.includes(path)) continue;
|
||||
if (entry.isDirectory()) yield* sourceFiles(path);
|
||||
else if (/\.(ts|tsx)$/.test(entry.name) && !isTestFile(entry.name))
|
||||
yield path;
|
||||
|
||||
+15
-19
@@ -5,19 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/* The styles Element Call needs wherever it is shown: the design tokens, fonts
|
||||
and element defaults its own stylesheets build on top of.
|
||||
/* The styles Element Call needs wherever it is shown: the element defaults and
|
||||
custom properties its own stylesheets build on top of.
|
||||
|
||||
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
|
||||
host's own document. What remains here still speaks of the
|
||||
document — normalize.css and the typography below use bare element selectors,
|
||||
and the custom properties are declared on `:root` — which is right for the
|
||||
page, and is why the component build rewrites it: there every selector is
|
||||
confined to Element Call's root element (see component/build/scopeStylesToRoot.ts),
|
||||
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
|
||||
reaching the host's document either.
|
||||
host's own document. Compound's stylesheets — the design tokens, the component
|
||||
styles and the fonts they use — are not here either: as a component, Element
|
||||
Call shares the host's copy of Compound (it is a peer dependency, so that what
|
||||
Compound floats into the host's body, tooltips and menus, is styled by the
|
||||
host's stylesheet), and index.css loads it for the page.
|
||||
|
||||
What remains here still speaks of the document — normalize.css and the
|
||||
typography below use bare element selectors, and the custom properties are
|
||||
declared on `:root` — which is right for the page, and is why the component
|
||||
build rewrites it: there every selector is confined to Element Call's root
|
||||
element (see component/build/scopeStylesToRoot.ts), 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 reaching the host's document either.
|
||||
|
||||
Nothing here should depend on where it lands relative to Element Call's
|
||||
component stylesheets: the bundler decides that, and it decides differently for
|
||||
@@ -25,16 +30,7 @@ the app and for the component build. */
|
||||
|
||||
@layer normalize, compound-legacy, compound;
|
||||
|
||||
@import url("@fontsource/inter/400.css");
|
||||
@import url("@fontsource/inter/500.css");
|
||||
@import url("@fontsource/inter/600.css");
|
||||
@import url("@fontsource/inter/700.css");
|
||||
@import url("@fontsource/inconsolata/400.css");
|
||||
@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-web/dist/style.css") layer(compound.components);
|
||||
|
||||
:root {
|
||||
--font-scale: 1;
|
||||
|
||||
+15
-1
@@ -7,10 +7,24 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
/* Styles for Element Call as a page of its own. The parts that apply wherever
|
||||
Element Call is shown live in base.css; these are about owning the document,
|
||||
and are not loaded when a host embeds Element Call as a component. */
|
||||
and are not loaded when a host embeds Element Call as a component.
|
||||
|
||||
That includes Compound: the page brings the design tokens, the component styles
|
||||
and their fonts, where a host embedding the component supplies its own. The
|
||||
`compound` layers are ordered by base.css. */
|
||||
|
||||
@import url("./base.css");
|
||||
|
||||
@import url("@fontsource/inter/400.css");
|
||||
@import url("@fontsource/inter/500.css");
|
||||
@import url("@fontsource/inter/600.css");
|
||||
@import url("@fontsource/inter/700.css");
|
||||
@import url("@fontsource/inconsolata/400.css");
|
||||
@import url("@fontsource/inconsolata/700.css");
|
||||
|
||||
@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);
|
||||
|
||||
body {
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
color: var(--cpd-color-text-primary);
|
||||
|
||||
@@ -65,8 +65,12 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
rollupOptions: {
|
||||
// The host already has these, and a second copy of any of them does not
|
||||
// merely bloat the bundle: React would hold two sets of hooks, and the
|
||||
// Matrix client would run two sync loops.
|
||||
// merely bloat the bundle: React would hold two sets of hooks, the
|
||||
// Matrix client would run two sync loops, and a second Compound would
|
||||
// bring a second stylesheet with class names of its own — so that what
|
||||
// it floats into the host's body, tooltips and menus, would match the
|
||||
// host's Compound stylesheet only when the two happened to be the same
|
||||
// version.
|
||||
//
|
||||
// Every subpath has to be named. Element Call reaches most of the Matrix
|
||||
// SDK as `matrix-js-sdk/lib/…`, and a bare "matrix-js-sdk" would not
|
||||
@@ -103,6 +107,8 @@ export default defineConfig(({ mode }) => {
|
||||
"matrix-js-sdk/lib/sync",
|
||||
"matrix-js-sdk/lib/types",
|
||||
"matrix-js-sdk/lib/utils",
|
||||
"@vector-im/compound-web",
|
||||
"@vector-im/compound-design-tokens/assets/web/icons",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ export default defineConfig((configEnv) =>
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"component/**/*.test.ts",
|
||||
"component/**/*.test.tsx",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user