Files
element-call/src/useTheme.ts
T
Valere 856e055d30 Find Element Call's root by attribute, not by being the body
Six selectors named `body` directly — the gradient backdrop and the
platform font overrides in index.css, and the iOS adjustments in
AppBar.module.css and Modal.module.css — so they only applied when Element
Call owned the page. A host mounting it into a container would have got an
interface decorated correctly and styled incorrectly, with nothing to show
that anything was wrong.

Mark the root element with data-element-call-root and match on that
instead. Scoping this way keeps the selectors more specific than they were,
rather than less: widening them to a bare [data-platform=…] would have
dropped specificity from (0,1,1) to (0,1,0) and changed which rules win.

The platform attribute moves with them, from the initializer's write onto
document.body to a layout effect on the root, alongside the theme — so it
still lands before anything is painted.

No visual change while Element Call owns the page: the root is the body,
which now carries the attribute, so every rewritten selector matches the
element it always did.
2026-09-03 12:55:52 +02:00

60 lines
2.1 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
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 => {
const rootElement = useRootElement();
const hostBridge = useHostBridge();
const { theme } = useUrlParams();
const [requestedTheme, setRequestedTheme] = useState(theme);
const previousTheme = useRef<string | null>(rootElement.classList.item(0));
useEffect(() => {
const subscription = hostBridge.themeChange$.subscribe(
({ data, reply }) => {
if (typeof data.name === "string") setRequestedTheme(data.name);
reply();
},
);
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";
const themeHighContrast = requestedTheme?.includes("high-contrast")
? "-hc"
: "";
const themeString = "cpd-theme-" + theme + themeHighContrast;
if (themeString !== previousTheme.current) {
rootElement.classList.remove(
"cpd-theme-light",
"cpd-theme-dark",
"cpd-theme-light-hc",
"cpd-theme-dark-hc",
);
rootElement.classList.add(themeString);
previousTheme.current = themeString;
}
rootElement.classList.remove("no-theme");
}, [previousTheme, requestedTheme, rootElement]);
};