diff --git a/src/AppBar.module.css b/src/AppBar.module.css index cc63854e7..faf2b0bf1 100644 --- a/src/AppBar.module.css +++ b/src/AppBar.module.css @@ -131,7 +131,7 @@ } } -body[data-platform="ios"] { +[data-element-call-root][data-platform="ios"] { .bar > header { grid-template-rows: minmax(var(--cpd-space-11x), auto) var(--cpd-space-4x); grid-template-areas: "primaryButton title secondaryButton"; diff --git a/src/Modal.module.css b/src/Modal.module.css index ae8006a53..303272091 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -48,7 +48,7 @@ Please see LICENSE in the repository root for full details. --handle-inset-block-end: var(--cpd-space-4x); } -body[data-platform="ios"] .drawer { +[data-element-call-root][data-platform="ios"] .drawer { --border-radius: 10px; --handle-block-size: 5px; --handle-inline-size: 36px; diff --git a/src/RootElementContext.ts b/src/RootElementContext.ts index d8211c74b..48dc26134 100644 --- a/src/RootElementContext.ts +++ b/src/RootElementContext.ts @@ -16,17 +16,18 @@ import { createContext, use } from "react"; * that when embedded in a host application it becomes the container the host * mounted it into, so that Element Call does not reach outside its own subtree. * - * That intent is not yet achievable: several selectors still name `body` - * directly — `body[data-background="gradient"]` and `body[data-platform=…]` in - * `index.css`, and `body[data-platform="ios"]` in `AppBar.module.css` and - * `Modal.module.css` — and `Initializer.initBeforeReact` writes - * `data-platform` straight onto the body. So anything other than the body will - * be decorated correctly and styled incorrectly, silently. Until those are - * scoped, treat this as preparation rather than a working seam. + * The stylesheets find this element by its `data-element-call-root` attribute, + * which {@link useTheme} sets along with the platform and theme, so they no + * longer depend on it being the body. + * + * What remains body-specific is the standalone page's own furniture: the + * `body` rule in `index.css` still sets the page background and margin, and + * `index.html` starts the body hidden with `no-theme` until the theme lands. + * Neither applies when a host mounts Element Call into a container of its own. */ // No provider is exported yet: nothing supplies a root element, so every -// consumer falls back to the document body. M1 adds one along with the -// component that mounts Element Call into a container. +// consumer falls back to the document body. One arrives with the entry point +// that mounts Element Call into a container. const RootElementContext = createContext(null); /** diff --git a/src/index.css b/src/index.css index 77db3394c..12f3951da 100644 --- a/src/index.css +++ b/src/index.css @@ -75,7 +75,7 @@ body { } @media (min-height: 330px) { - body[data-background="gradient"]::before { + [data-element-call-root][data-background="gradient"]::before { content: ""; position: fixed; /* Chromium abruptly fades our images to fully transparent at the edge of @@ -88,7 +88,7 @@ body { background-repeat: no-repeat; } - body[data-background="gradient"][data-platform="desktop"]::before { + [data-element-call-root][data-background="gradient"][data-platform="desktop"]::before { background-image: url("graphics/desktop-gradient.png"); background-size: max(1440px, 100vw) max(1440px, 100vh); background-position: center; @@ -126,11 +126,11 @@ body, /* 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 font-family. */ -body[data-platform="android"] { +[data-element-call-root][data-platform="android"] { --cpd-font-family-sans: "Roboto", "Noto", "Inter", sans-serif; } -body[data-platform="ios"] { +[data-element-call-root][data-platform="ios"] { --cpd-font-family-sans: -apple-system, BlinkMacSystemFont, "Inter", sans-serif; } diff --git a/src/initializer.tsx b/src/initializer.tsx index ee3ee9bd4..253dcbc41 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -30,7 +30,6 @@ import { import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; import { seedSettingsFromConfig } from "./settings/settings"; -import { platform } from "./Platform"; import { isFailure } from "./utils/fetch"; import { initializeWidget, type WidgetHelpers } from "./widget"; import { enableExtendedLivekitLogs } from "./settings/settings.ts"; @@ -229,9 +228,6 @@ export class Initializer { ); } - // Add the platform to the DOM, so CSS can query it - document.body.setAttribute("data-platform", platform); - // livekit logging configuration setLKLogExtension((level, msg, context) => { // we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index e5accd15d..fe0cbffb3 100644 --- a/src/useTheme.test.ts +++ b/src/useTheme.test.ts @@ -19,6 +19,7 @@ import { } from "vitest"; import { useTheme } from "./useTheme"; +import { platform } from "./Platform"; import { useUrlParams } from "./UrlParams"; import { type HostBridge, @@ -88,6 +89,20 @@ describe("useTheme", () => { expect(originalClassList.add).not.toHaveBeenCalled(); }); + test("marks the element as Element Call's root, for the stylesheets", () => { + renderHook(() => useTheme(), { wrapper }); + + // The stylesheets find the root by this rather than by naming `body`, so + // that they still apply when Element Call is mounted into a container + expect(document.body.hasAttribute("data-element-call-root")).toBe(true); + }); + + test("records the platform on the root, for the stylesheets", () => { + renderHook(() => useTheme(), { wrapper }); + + expect(document.body.getAttribute("data-platform")).toBe(platform); + }); + test("theme changes in response to host requests", () => { renderHook(() => useTheme(), { wrapper }); diff --git a/src/useTheme.ts b/src/useTheme.ts index 3c02745a6..86a6f63fd 100644 --- a/src/useTheme.ts +++ b/src/useTheme.ts @@ -9,6 +9,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { useUrlParams } from "./UrlParams"; import { useRootElement } from "./RootElementContext"; +import { platform } from "./Platform"; import { useHostBridge } from "./HostBridge"; export const useTheme = (): void => { @@ -28,6 +29,14 @@ export const useTheme = (): void => { return (): void => subscription.unsubscribe(); }, [hostBridge]); + // Mark the element as Element Call's root and record the platform on it, so + // that the stylesheets can find both without naming `body`. A layout effect, + // like the theme below, so that it lands before anything is painted. + useLayoutEffect(() => { + rootElement.setAttribute("data-element-call-root", ""); + rootElement.setAttribute("data-platform", platform); + }, [rootElement]); + useLayoutEffect(() => { // If no theme has been explicitly requested we default to dark const theme = requestedTheme?.includes("light") ? "light" : "dark";