mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-13 23:35:51 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b72493f47f
commit
9d0a12c38a
@@ -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 => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip label="Microphone">
|
||||||
|
<button>🎤</button>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
test("floats into the host document with no root", () => {
|
||||||
|
render(
|
||||||
|
<CallPortalRoot root={undefined}>
|
||||||
|
<LabelledButton />
|
||||||
|
</CallPortalRoot>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<CallPortalRoot root={root}>
|
||||||
|
<LabelledButton />
|
||||||
|
</CallPortalRoot>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<CallPortalRoot root={null}>
|
||||||
|
<LabelledButton />
|
||||||
|
</CallPortalRoot>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("button", { name: "Microphone" })).toBeNull();
|
||||||
|
});
|
||||||
@@ -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<Props> = ({ root, children }): ReactNode =>
|
||||||
|
root === undefined ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<PortalRoot root={root}>{children}</PortalRoot>
|
||||||
|
);
|
||||||
+3
-1
@@ -130,7 +130,9 @@ export interface ElementCallProps {
|
|||||||
* A host that moves Element Call's DOM into another document, such as a
|
* 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
|
* 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
|
* 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;
|
portalRoot?: HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -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 { PortalRoot, TooltipProvider } from "@vector-im/compound-web";
|
import { 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";
|
||||||
@@ -82,6 +82,7 @@ import { useTheme } from "../src/useTheme";
|
|||||||
import { useStableValue } from "../src/useStableValue";
|
import { useStableValue } from "../src/useStableValue";
|
||||||
import styles from "./ElementCall.module.css";
|
import styles from "./ElementCall.module.css";
|
||||||
import { useComponentHostBridge } from "./host";
|
import { useComponentHostBridge } from "./host";
|
||||||
|
import { CallPortalRoot } from "./CallPortalRoot";
|
||||||
import { type ElementCallProps } from "./api";
|
import { type ElementCallProps } from "./api";
|
||||||
import { supportedLanguages, translationsBackend } from "./localization";
|
import { supportedLanguages, translationsBackend } from "./localization";
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
ref,
|
ref,
|
||||||
theme,
|
theme,
|
||||||
language,
|
language,
|
||||||
portalRoot = null,
|
portalRoot,
|
||||||
}): ReactNode => {
|
}): ReactNode => {
|
||||||
const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme);
|
const hostBridge = useComponentHostBridge(suppliedHostBridge, ref, theme);
|
||||||
|
|
||||||
@@ -241,9 +242,7 @@ 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}>
|
||||||
{/* Where Compound floats the call's menus and tooltips: the
|
<CallPortalRoot root={portalRoot}>
|
||||||
host's body unless the host says otherwise (see `portalRoot`) */}
|
|
||||||
<PortalRoot root={portalRoot}>
|
|
||||||
<RootElementProvider value={container}>
|
<RootElementProvider value={container}>
|
||||||
{/* Whatever goes wrong in here is shown in here. Left to
|
{/* Whatever goes wrong in here is shown in here. Left to
|
||||||
propagate, an error would unmount the host's own tree. */}
|
propagate, an error would unmount the host's own tree. */}
|
||||||
@@ -272,7 +271,7 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
</Decoration>
|
</Decoration>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</RootElementProvider>
|
</RootElementProvider>
|
||||||
</PortalRoot>
|
</CallPortalRoot>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user