mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-13 23:35:51 +00:00
[ElementCallComponent] Use compound-web's PortalRoot in EC.
This commit is contained in:
@@ -121,4 +121,16 @@ export interface ElementCallProps {
|
|||||||
* the most recently set language wins for all of them.
|
* the most recently set language wins for all of them.
|
||||||
*/
|
*/
|
||||||
language?: string;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-29
@@ -50,7 +50,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { I18nextProvider } from "react-i18next";
|
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 { ErrorBoundary } from "@sentry/react";
|
||||||
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
|
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
|
||||||
import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js";
|
import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js";
|
||||||
@@ -154,6 +154,7 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
ref,
|
ref,
|
||||||
theme,
|
theme,
|
||||||
language,
|
language,
|
||||||
|
portalRoot = null,
|
||||||
}): ReactNode => {
|
}): ReactNode => {
|
||||||
const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme);
|
const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme);
|
||||||
|
|
||||||
@@ -240,34 +241,38 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
// Stands in for the standalone page's `#root`: see `.content`
|
// Stands in for the standalone page's `#root`: see `.content`
|
||||||
// in ElementCall.module.css
|
// in ElementCall.module.css
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<RootElementProvider value={container}>
|
{/* Where Compound floats the call's menus and tooltips: the
|
||||||
{/* Whatever goes wrong in here is shown in here. Left to
|
host's body unless the host says otherwise (see `portalRoot`) */}
|
||||||
propagate, an error would unmount the host's own tree. */}
|
<PortalRoot root={portalRoot}>
|
||||||
<ErrorBoundary
|
<RootElementProvider value={container}>
|
||||||
fallback={(error) => <ErrorPage error={error} />}
|
{/* Whatever goes wrong in here is shown in here. Left to
|
||||||
// A broken call should not hold the host on screen
|
propagate, an error would unmount the host's own tree. */}
|
||||||
onError={() => void hostBridge.setAlwaysOnScreen(false)}
|
<ErrorBoundary
|
||||||
>
|
fallback={(error) => <ErrorPage error={error} />}
|
||||||
<Decoration>
|
// A broken call should not hold the host on screen
|
||||||
<TooltipProvider>
|
onError={() => void hostBridge.setAlwaysOnScreen(false)}
|
||||||
<ClientProvider client={client}>
|
>
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<Decoration>
|
||||||
<ProcessorProvider>
|
<TooltipProvider>
|
||||||
<CallView
|
<ClientProvider client={client}>
|
||||||
client={client}
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
rtcSession={rtcSession}
|
<ProcessorProvider>
|
||||||
isPasswordlessUser={false}
|
<CallView
|
||||||
confineToRoom={params.confineToRoom}
|
client={client}
|
||||||
preload={params.preload}
|
rtcSession={rtcSession}
|
||||||
skipLobby={params.skipLobby}
|
isPasswordlessUser={false}
|
||||||
/>
|
confineToRoom={params.confineToRoom}
|
||||||
</ProcessorProvider>
|
preload={params.preload}
|
||||||
</MediaDevicesContext>
|
skipLobby={params.skipLobby}
|
||||||
</ClientProvider>
|
/>
|
||||||
</TooltipProvider>
|
</ProcessorProvider>
|
||||||
</Decoration>
|
</MediaDevicesContext>
|
||||||
</ErrorBoundary>
|
</ClientProvider>
|
||||||
</RootElementProvider>
|
</TooltipProvider>
|
||||||
|
</Decoration>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</RootElementProvider>
|
||||||
|
</PortalRoot>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@vector-im/compound-design-tokens": "^11.0.0",
|
"@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": "*",
|
"matrix-js-sdk": "*",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19"
|
"react-dom": "^19"
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@
|
|||||||
"@typescript-eslint/utils": "^8.61.0",
|
"@typescript-eslint/utils": "^8.61.0",
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@use-gesture/react": "^10.2.11",
|
||||||
"@vector-im/compound-design-tokens": "^11.0.0",
|
"@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",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@vitest/browser-playwright": "^4.1.5",
|
"@vitest/browser-playwright": "^4.1.5",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
|
|||||||
Generated
+7
-6
@@ -134,8 +134,8 @@ importers:
|
|||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.1.0(@types/react@19.2.18)(react@19.2.8)
|
version: 11.1.0(@types/react@19.2.18)(react@19.2.8)
|
||||||
'@vector-im/compound-web':
|
'@vector-im/compound-web':
|
||||||
specifier: ^10.0.0
|
specifier: github:element-hq/compound-web#abdd1657b4787952b19203c7ce81fda02822ac7f
|
||||||
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)
|
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':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.0.2
|
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))
|
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:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vector-im/compound-web@10.1.0':
|
'@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f':
|
||||||
resolution: {integrity: sha512-Lhw9Q+RZo1GtDYjF5PXWTD0zdfbrz/vnSVBYFHC8Hq+YeDule2QfGbTRo8IVHeV7jK6ZzsLc8zb6553eeIRLYg==}
|
resolution: {gitHosted: true, integrity: sha512-HMN3hCJQyL7PS7ChreMN2OC2AFeA5og0vmSV86v727qWQP5He2cho8JGwoIhpe50awV6cMrp37XEIkVLqW5UaA==, tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/abdd1657b4787952b19203c7ce81fda02822ac7f}
|
||||||
|
version: 10.3.1
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@fontsource/inconsolata': ^5
|
'@fontsource/inconsolata': ^5
|
||||||
'@fontsource/inter': ^5
|
'@fontsource/inter': ^5
|
||||||
'@types/react': '*'
|
'@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
|
react: ^18 || ^19.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
@@ -8672,7 +8673,7 @@ snapshots:
|
|||||||
'@types/react': 19.2.18
|
'@types/react': 19.2.18
|
||||||
react: 19.2.8
|
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:
|
dependencies:
|
||||||
'@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
'@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)
|
'@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ allowBuilds:
|
|||||||
"core-js": true
|
"core-js": true
|
||||||
"esbuild": true
|
"esbuild": true
|
||||||
"matrix-js-sdk@git+https://github.com/matrix-org/matrix-js-sdk.git": 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
|
"protobufjs": true
|
||||||
overrides:
|
overrides:
|
||||||
# We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but
|
# We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { type ReactNode, useState } from "react";
|
|||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
|
import { RootElementProvider } from "./RootElementContext";
|
||||||
|
|
||||||
const originalMatchMedia = window.matchMedia;
|
const originalMatchMedia = window.matchMedia;
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -70,3 +71,39 @@ test("the modal renders as a drawer in mobile viewports", () => {
|
|||||||
);
|
);
|
||||||
expect(queryByRole("dialog")).toMatchSnapshot();
|
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 (
|
||||||
|
<RootElementProvider value={otherBody}>
|
||||||
|
<Modal title="My modal" open={isOpen} onDismiss={() => setOpen(false)}>
|
||||||
|
<p>This is the content.</p>
|
||||||
|
</Modal>
|
||||||
|
</RootElementProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render(<ModalFn />);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ export const Modal: FC<Props> = ({
|
|||||||
<DialogPortal container={rootElement}>
|
<DialogPortal container={rootElement}>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||||
|
// Radix dismisses on a pointer down outside the dialog, but only
|
||||||
|
// for event targets that are a `Node` of the window it was loaded
|
||||||
|
// in. A host can move the dialog into another window (a Document
|
||||||
|
// Picture-in-Picture window), whose nodes fail that check, so the
|
||||||
|
// backdrop closes the dialog itself.
|
||||||
|
onClick={() => onDismiss?.()}
|
||||||
/>
|
/>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ exports[`AppBar > renders 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -58,7 +58,7 @@ exports[`AppBar > renders with title and subtitle 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -111,7 +111,7 @@ exports[`LobbyView > renders with AppBar android 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -247,7 +247,7 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -343,7 +343,7 @@ exports[`LobbyView > renders with AppBar ios 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -621,7 +621,7 @@ exports[`LobbyView > renders with header and participant count 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_147l5_17"
|
class="_indicator-icon_j097f_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
Reference in New Issue
Block a user