From f322af2742d2f2a8c6293566fda5c6f1fe16b03d Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Thu, 8 Oct 2026 19:14:01 +0200 Subject: [PATCH 1/7] [ElementCallComponent] Use compound-web's PortalRoot in EC. --- component/api.ts | 12 ++++ component/index.tsx | 63 ++++++++++--------- component/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 13 ++-- pnpm-workspace.yaml | 3 + src/Modal.test.tsx | 37 +++++++++++ src/Modal.tsx | 6 ++ src/__snapshots__/AppBar.test.tsx.snap | 4 +- .../__snapshots__/InCallView.test.tsx.snap | 2 +- .../__snapshots__/LobbyView.test.tsx.snap | 10 +-- 11 files changed, 109 insertions(+), 45 deletions(-) diff --git a/component/api.ts b/component/api.ts index c9e7a2fd7..726eb1f9c 100644 --- a/component/api.ts +++ b/component/api.ts @@ -121,4 +121,16 @@ export interface ElementCallProps { * the most recently set language wins for all of them. */ language?: string; + /** + * Where the parts of Element Call that float above it — its menus and + * tooltips, which Compound portals out of the call — are rendered. Left out, + * they go into the host document's body, as they do for any other component + * on the host's page. + * + * A host that moves Element Call's DOM into another document, such as a + * Document Picture-in-Picture window, passes that document's body here so + * that a menu opens in the window its button is in. Changes take effect at + * once. + */ + portalRoot?: HTMLElement | null; } diff --git a/component/index.tsx b/component/index.tsx index 8f901c139..9f7d7d49a 100644 --- a/component/index.tsx +++ b/component/index.tsx @@ -50,7 +50,7 @@ import { } from "react"; import { logger } from "matrix-js-sdk/lib/logger"; import { I18nextProvider } from "react-i18next"; -import { TooltipProvider } from "@vector-im/compound-web"; +import { PortalRoot, TooltipProvider } from "@vector-im/compound-web"; import { ErrorBoundary } from "@sentry/react"; import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js"; @@ -154,6 +154,7 @@ export const ElementCall: FC = ({ ref, theme, language, + portalRoot = null, }): ReactNode => { const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme); @@ -240,34 +241,38 @@ export const ElementCall: FC = ({ // Stands in for the standalone page's `#root`: see `.content` // in ElementCall.module.css
- - {/* Whatever goes wrong in here is shown in here. Left to - propagate, an error would unmount the host's own tree. */} - } - // A broken call should not hold the host on screen - onError={() => void hostBridge.setAlwaysOnScreen(false)} - > - - - - - - - - - - - - - + {/* Where Compound floats the call's menus and tooltips: the + host's body unless the host says otherwise (see `portalRoot`) */} + + + {/* Whatever goes wrong in here is shown in here. Left to + propagate, an error would unmount the host's own tree. */} + } + // A broken call should not hold the host on screen + onError={() => void hostBridge.setAlwaysOnScreen(false)} + > + + + + + + + + + + + + + +
)} diff --git a/component/package.json b/component/package.json index 0d45eb4d7..9929b39bb 100644 --- a/component/package.json +++ b/component/package.json @@ -39,7 +39,7 @@ }, "peerDependencies": { "@vector-im/compound-design-tokens": "^11.0.0", - "@vector-im/compound-web": "^10.0.0", + "@vector-im/compound-web": "^10.3.1", "matrix-js-sdk": "*", "react": "^19", "react-dom": "^19" diff --git a/package.json b/package.json index b30f57ce3..a74dca7fa 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@typescript-eslint/utils": "^8.61.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^11.0.0", - "@vector-im/compound-web": "^10.0.0", + "@vector-im/compound-web": "github:element-hq/compound-web#abdd1657b4787952b19203c7ce81fda02822ac7f", "@vitejs/plugin-react": "^6.0.2", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69805cfcd..cd18698ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,8 +134,8 @@ importers: specifier: ^11.0.0 version: 11.1.0(@types/react@19.2.18)(react@19.2.8) '@vector-im/compound-web': - specifier: ^10.0.0 - version: 10.1.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: github:element-hq/compound-web#abdd1657b4787952b19203c7ce81fda02822ac7f + version: https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vitejs/plugin-react': specifier: ^6.0.2 version: 6.1.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.2.7)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)) @@ -3240,13 +3240,14 @@ packages: react: optional: true - '@vector-im/compound-web@10.1.0': - resolution: {integrity: sha512-Lhw9Q+RZo1GtDYjF5PXWTD0zdfbrz/vnSVBYFHC8Hq+YeDule2QfGbTRo8IVHeV7jK6ZzsLc8zb6553eeIRLYg==} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f': + resolution: {gitHosted: true, integrity: sha512-HMN3hCJQyL7PS7ChreMN2OC2AFeA5og0vmSV86v727qWQP5He2cho8JGwoIhpe50awV6cMrp37XEIkVLqW5UaA==, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f} + version: 10.3.1 peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 '@types/react': '*' - '@vector-im/compound-design-tokens': '>=1.6.1 <11.0.0' + '@vector-im/compound-design-tokens': '>=1.6.1 <12.0.0' react: ^18 || ^19.0.0 peerDependenciesMeta: '@types/react': @@ -8672,7 +8673,7 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@vector-im/compound-web@10.1.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9e4f0fd6f..6728743cd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,9 @@ allowBuilds: "core-js": true "esbuild": true "matrix-js-sdk@git+https://github.com/matrix-org/matrix-js-sdk.git": true + # Compound is a git dependency until PortalRoot (element-hq/compound-web#562) is released; its + # `prepare` script builds it on install. pnpm matches git-hosted packages by repository URL. + "@vector-im/compound-web@git+https://github.com/element-hq/compound-web.git": true "protobufjs": true overrides: # We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx index 74670b6db..1c67bd4da 100644 --- a/src/Modal.test.tsx +++ b/src/Modal.test.tsx @@ -11,6 +11,7 @@ import { type ReactNode, useState } from "react"; import userEvent from "@testing-library/user-event"; import { Modal } from "./Modal"; +import { RootElementProvider } from "./RootElementContext"; const originalMatchMedia = window.matchMedia; afterEach(() => { @@ -70,3 +71,39 @@ test("the modal renders as a drawer in mobile viewports", () => { ); expect(queryByRole("dialog")).toMatchSnapshot(); }); + +test("the modal can be closed by clicking the backdrop, in another window too", async () => { + // The drawer test above leaves a touchscreen matchMedia behind; this is + // about the desktop dialog + window.matchMedia = (): MediaQueryList => + ({ + matches: false, + addEventListener: (): void => {}, + removeEventListener: (): void => {}, + }) as unknown as MediaQueryList; + // A dialog moved into another window, as a host does for a Document + // Picture-in-Picture window: its nodes are not `Node`s of this window + const frame = document.createElement("iframe"); + document.body.appendChild(frame); + const otherBody = frame.contentDocument!.body; + + function ModalFn(): ReactNode { + const [isOpen, setOpen] = useState(true); + return ( + + setOpen(false)}> +

This is the content.

+
+
+ ); + } + render(); + const dialog = otherBody.querySelector('[role="dialog"]'); + expect(dialog).not.toBeNull(); + const backdrop = dialog!.previousElementSibling as HTMLElement; + + const user = userEvent.setup({ document: frame.contentDocument! }); + await user.click(backdrop); + expect(otherBody.querySelector('[role="dialog"]')).toBeNull(); + frame.remove(); +}); diff --git a/src/Modal.tsx b/src/Modal.tsx index 46316d370..01b2c76bc 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -160,6 +160,12 @@ export const Modal: FC = ({ onDismiss?.()} /> renders 1`] = ` tabindex="0" >
renders with title and subtitle 1`] = ` tabindex="0" >
rendering > renders 1`] = ` tabindex="0" >
renders with AppBar android 1`] = ` tabindex="0" >
renders with AppBar android 1`] = ` tabindex="0" >
renders with AppBar ios 1`] = ` tabindex="0" >
renders with AppBar ios 1`] = ` tabindex="0" >
renders with header and participant count 1`] = ` tabindex="0" >
Date: Thu, 8 Oct 2026 21:26:34 +0200 Subject: [PATCH 2/7] bump compound-web --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a74dca7fa..983957f10 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@typescript-eslint/utils": "^8.61.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^11.0.0", - "@vector-im/compound-web": "github:element-hq/compound-web#abdd1657b4787952b19203c7ce81fda02822ac7f", + "@vector-im/compound-web": "github:element-hq/compound-web#65fbe557780a53f93e55e17468613d2899e2d2a3", "@vitejs/plugin-react": "^6.0.2", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd18698ec..af303f207 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,8 +134,8 @@ importers: specifier: ^11.0.0 version: 11.1.0(@types/react@19.2.18)(react@19.2.8) '@vector-im/compound-web': - specifier: github:element-hq/compound-web#abdd1657b4787952b19203c7ce81fda02822ac7f - version: https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: github:element-hq/compound-web#65fbe557780a53f93e55e17468613d2899e2d2a3 + version: https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vitejs/plugin-react': specifier: ^6.0.2 version: 6.1.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.2.7)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)) @@ -3240,8 +3240,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f': - resolution: {gitHosted: true, integrity: sha512-HMN3hCJQyL7PS7ChreMN2OC2AFeA5og0vmSV86v727qWQP5He2cho8JGwoIhpe50awV6cMrp37XEIkVLqW5UaA==, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3': + resolution: {gitHosted: true, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3} version: 10.3.1 peerDependencies: '@fontsource/inconsolata': ^5 @@ -8673,7 +8673,7 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) From c3d07b89bdeabb161d36a3e9e33068e13426b26f Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 9 Oct 2026 11:08:03 +0200 Subject: [PATCH 3/7] review: unnecassary matchMedia overwrite on test start. --- src/Modal.test.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx index 1c67bd4da..223bbbc8d 100644 --- a/src/Modal.test.tsx +++ b/src/Modal.test.tsx @@ -73,14 +73,6 @@ test("the modal renders as a drawer in mobile viewports", () => { }); test("the modal can be closed by clicking the backdrop, in another window too", async () => { - // The drawer test above leaves a touchscreen matchMedia behind; this is - // about the desktop dialog - window.matchMedia = (): MediaQueryList => - ({ - matches: false, - addEventListener: (): void => {}, - removeEventListener: (): void => {}, - }) as unknown as MediaQueryList; // A dialog moved into another window, as a host does for a Document // Picture-in-Picture window: its nodes are not `Node`s of this window const frame = document.createElement("iframe"); From 674692c9265dc9fc17b078630c5a74f64ef8ae34 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 9 Oct 2026 11:09:41 +0200 Subject: [PATCH 4/7] Fix breaking Ci tests due to compound-web 10.1.0 -> 10.3.1 bump --- playwright/component/audio-menu.spec.ts | 2 +- src/components/MediaMuteAndSwitchButton.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright/component/audio-menu.spec.ts b/playwright/component/audio-menu.spec.ts index f4d811598..3276c28a7 100644 --- a/playwright/component/audio-menu.spec.ts +++ b/playwright/component/audio-menu.spec.ts @@ -129,7 +129,7 @@ async function openDeviceList(page: Page, pane: Locator): Promise { .getByRole("button", { name: "Microphone" }) .click({ timeout: 60_000 }); await expect(page.getByRole("menu")).toBeVisible(); - const list = page.locator("[role='menu'] div[role='none']").first(); + const list = page.getByRole("menu").getByTestId("device-list"); await expect(list).toBeVisible(); return list; } diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx index 25342f001..cb2dbec39 100644 --- a/src/components/MediaMuteAndSwitchButton.tsx +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -345,6 +345,7 @@ export const MediaMuteAndSwitchButton: FC = ({ // Keeps the items the menu's own children for assistive tech. role="none" className={styles.deviceList} + data-testid="device-list" style={ { "--device-list-max-height": From c79675e6a9e5c54eac4170ed2996496bc65e25b8 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Mon, 12 Oct 2026 10:14:49 +0200 Subject: [PATCH 5/7] bump to compound-web main branch --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 983957f10..7f2c24da5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@typescript-eslint/utils": "^8.61.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^11.0.0", - "@vector-im/compound-web": "github:element-hq/compound-web#65fbe557780a53f93e55e17468613d2899e2d2a3", + "@vector-im/compound-web": "github:element-hq/compound-web#92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669", "@vitejs/plugin-react": "^6.0.2", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af303f207..91ef21a82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,8 +134,8 @@ importers: specifier: ^11.0.0 version: 11.1.0(@types/react@19.2.18)(react@19.2.8) '@vector-im/compound-web': - specifier: github:element-hq/compound-web#65fbe557780a53f93e55e17468613d2899e2d2a3 - version: https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: github:element-hq/compound-web#92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669 + version: https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vitejs/plugin-react': specifier: ^6.0.2 version: 6.1.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.2.7)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)) @@ -3240,8 +3240,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3': - resolution: {gitHosted: true, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669': + resolution: {gitHosted: true, integrity: sha512-tna4jBJiDKrr9x9eG62ai0RKB2CDZNs2c/OuGPT/DbJ/gVXoQ4uFtOSSjExnDtIrYd4VIm/DpZUs1O4JH6EP9Q==, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669} version: 10.3.1 peerDependencies: '@fontsource/inconsolata': ^5 @@ -8673,7 +8673,7 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/65fbe557780a53f93e55e17468613d2899e2d2a3(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) From b72493f47f27cc6ed4d396413e1a84cb1abec1f3 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Mon, 12 Oct 2026 11:15:03 +0200 Subject: [PATCH 6/7] bump compound-web to release 10.4.0 --- package.json | 2 +- pnpm-lock.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7f2c24da5..3c7623366 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@typescript-eslint/utils": "^8.61.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^11.0.0", - "@vector-im/compound-web": "github:element-hq/compound-web#92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669", + "@vector-im/compound-web": "^10.4.0", "@vitejs/plugin-react": "^6.0.2", "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91ef21a82..da411cd4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,8 +134,8 @@ importers: specifier: ^11.0.0 version: 11.1.0(@types/react@19.2.18)(react@19.2.8) '@vector-im/compound-web': - specifier: github:element-hq/compound-web#92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669 - version: https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^10.4.0 + version: 10.4.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vitejs/plugin-react': specifier: ^6.0.2 version: 6.1.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.2.7)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.103.1)(terser@5.46.1)(yaml@2.9.0)) @@ -3240,9 +3240,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669': - resolution: {gitHosted: true, integrity: sha512-tna4jBJiDKrr9x9eG62ai0RKB2CDZNs2c/OuGPT/DbJ/gVXoQ4uFtOSSjExnDtIrYd4VIm/DpZUs1O4JH6EP9Q==, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669} - version: 10.3.1 + '@vector-im/compound-web@10.4.0': + resolution: {integrity: sha512-Tt5hsNK7ulhGdRuy9lzSm5C+T8M/4O4kLDFLkaU+Hi1kVWj6tj/5awW+U+GvtpWRh5llbHN2HRCh79+gQhek+A==} peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -8673,7 +8672,7 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/92bbc3e5cea06ea8dd8b22e2835b4f37b4d6a669(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@vector-im/compound-web@10.4.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@vector-im/compound-design-tokens@11.1.0(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) From 9d0a12c38af9f349243dbda16b9e7deb84d9fa70 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Mon, 12 Oct 2026 12:29:39 +0200 Subject: [PATCH 7/7] Fix PortalRoot hiding the component's menus and tooltips compound-web 10.4.0 treats a PortalRoot whose root is null as one whose root is not available yet, so floating-ui never mounts the portal and every tooltip label, and with it every icon button's accessible name, is missing. The component defaulted portalRoot to null and wrapped the call unconditionally. Wrap the call only when the host supplied a root, keeping null as the not-yet-available case Compound defines, and say so on the prop. Cover it in CallPortalRoot.test.tsx. Drop the git allowBuilds entry for compound-web now that it is a release again. Co-Authored-By: Claude Fable 5.1 --- component/CallPortalRoot.test.tsx | 57 +++++++++++++++++++++++++++++++ component/CallPortalRoot.tsx | 26 ++++++++++++++ component/api.ts | 4 ++- component/index.tsx | 11 +++--- pnpm-workspace.yaml | 3 -- 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 component/CallPortalRoot.test.tsx create mode 100644 component/CallPortalRoot.tsx diff --git a/component/CallPortalRoot.test.tsx b/component/CallPortalRoot.test.tsx new file mode 100644 index 000000000..bfa23971b --- /dev/null +++ b/component/CallPortalRoot.test.tsx @@ -0,0 +1,57 @@ +/* +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 { expect, test } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { type ReactNode } from "react"; +import { Tooltip, TooltipProvider } from "@vector-im/compound-web"; + +import { CallPortalRoot } from "./CallPortalRoot"; + +// A button whose accessible name comes from its tooltip, as every icon button +// in the call does: the name is lost whenever the tooltip has nowhere to go. +const LabelledButton = (): ReactNode => ( + + + + + +); + +test("floats into the host document with no root", () => { + render( + + + , + ); + expect(screen.getByRole("button", { name: "Microphone" })).toBeTruthy(); +}); + +test("floats into the root it is given", () => { + const root = document.createElement("div"); + document.body.appendChild(root); + render( + + + , + ); + const button = screen.getByRole("button", { name: "Microphone" }); + const label = document.getElementById( + button.getAttribute("aria-labelledby")!, + ); + expect(root.contains(label)).toBe(true); + root.remove(); +}); + +test("waits for a root that is not available yet", () => { + render( + + + , + ); + expect(screen.queryByRole("button", { name: "Microphone" })).toBeNull(); +}); diff --git a/component/CallPortalRoot.tsx b/component/CallPortalRoot.tsx new file mode 100644 index 000000000..281eb28c0 --- /dev/null +++ b/component/CallPortalRoot.tsx @@ -0,0 +1,26 @@ +/* +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 { type FC, type ReactNode } from "react"; +import { PortalRoot } from "@vector-im/compound-web"; + +interface Props { + root: HTMLElement | null | undefined; + children: ReactNode; +} + +/** + * Where Compound floats the call's menus and tooltips. With no `root` the call + * is not wrapped at all and they go into the host document's body; `null` is + * Compound's "not available yet", and the floating parts wait for it. + */ +export const CallPortalRoot: FC = ({ root, children }): ReactNode => + root === undefined ? ( + children + ) : ( + {children} + ); diff --git a/component/api.ts b/component/api.ts index 726eb1f9c..fa2928eb1 100644 --- a/component/api.ts +++ b/component/api.ts @@ -130,7 +130,9 @@ export interface ElementCallProps { * A host that moves Element Call's DOM into another document, such as a * Document Picture-in-Picture window, passes that document's body here so * that a menu opens in the window its button is in. Changes take effect at - * once. + * once. `null` means the root is not available yet, as from a callback ref + * that has not fired: menus and tooltips wait for it rather than falling + * back to the body. */ portalRoot?: HTMLElement | null; } diff --git a/component/index.tsx b/component/index.tsx index 9f7d7d49a..b3f7b6209 100644 --- a/component/index.tsx +++ b/component/index.tsx @@ -50,7 +50,7 @@ import { } from "react"; import { logger } from "matrix-js-sdk/lib/logger"; import { I18nextProvider } from "react-i18next"; -import { PortalRoot, TooltipProvider } from "@vector-im/compound-web"; +import { TooltipProvider } from "@vector-im/compound-web"; import { ErrorBoundary } from "@sentry/react"; import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js"; @@ -82,6 +82,7 @@ import { useTheme } from "../src/useTheme"; import { useStableValue } from "../src/useStableValue"; import styles from "./ElementCall.module.css"; import { useComponentHostBridge } from "./host"; +import { CallPortalRoot } from "./CallPortalRoot"; import { type ElementCallProps } from "./api"; import { supportedLanguages, translationsBackend } from "./localization"; @@ -154,7 +155,7 @@ export const ElementCall: FC = ({ ref, theme, language, - portalRoot = null, + portalRoot, }): ReactNode => { const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme); @@ -241,9 +242,7 @@ export const ElementCall: FC = ({ // Stands in for the standalone page's `#root`: see `.content` // in ElementCall.module.css
- {/* Where Compound floats the call's menus and tooltips: the - host's body unless the host says otherwise (see `portalRoot`) */} - + {/* Whatever goes wrong in here is shown in here. Left to propagate, an error would unmount the host's own tree. */} @@ -272,7 +271,7 @@ export const ElementCall: FC = ({ - +
)}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6728743cd..9e4f0fd6f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,9 +14,6 @@ allowBuilds: "core-js": true "esbuild": true "matrix-js-sdk@git+https://github.com/matrix-org/matrix-js-sdk.git": true - # Compound is a git dependency until PortalRoot (element-hq/compound-web#562) is released; its - # `prepare` script builds it on install. pnpm matches git-hosted packages by repository URL. - "@vector-im/compound-web@git+https://github.com/element-hq/compound-web.git": true "protobufjs": true overrides: # We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but