Keep keyboard shortcuts within Element Call's root

The call's shortcuts were listened for on the window and allowed
whenever focus was inside the standalone app's `#root` — which, for a
component embedded in a host, is the host's own root, or nothing. So m,
v and space fired while the user typed in the host's composer, and two
Element Calls on one page both answered every key.

Listen on the element Element Call treats as its root instead (the body
standalone, so nothing changes there), and judge whether a key press is
spoken for by what has focus — a dialog or a text field — rather than by
where it sits in the DOM, since the modals are now portalled to that
same root.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Timo K.
2026-09-08 13:52:34 +02:00
co-authored by Claude Fable 5.1
parent 608f107f8d
commit bc58aed0d7
2 changed files with 109 additions and 34 deletions
+60 -3
View File
@@ -18,6 +18,7 @@ import {
ReactionsRowSize,
} from "./reactions";
import { type Controls } from "./controls";
import { RootElementProvider } from "./RootElementContext";
// Test Explanation:
// - The main objective is to test `useCallViewKeyboardShortcuts`.
@@ -48,10 +49,11 @@ const TestComponent: FC<TestComponentProps> = ({
);
return (
<>
<div id={initialModalOpen ? "root" : undefined}>
<div>
<Button onClick={onButtonClick}>TEST</Button>
</div>
{/*// modal lives outside of the root*/}
{/* A dialog, which is what claims key presses for itself; where it
lives in the DOM does not matter */}
{modalOpen && (
<dialog
open
@@ -165,7 +167,7 @@ test("unmuting happens in place of the default action", async () => {
// container element that can be interactive and receive focus / keydown
// events. <video> is kind of a weird choice, but it'll do the job.
render(
<div id="root">
<div>
<video
tabIndex={0}
onKeyDown={(e) => defaultPrevented(e.isDefaultPrevented())}
@@ -210,3 +212,58 @@ test("escape button does not trigger back if sth else is focused", async () => {
await user.keyboard("[Escape]");
expect(window.controls.onBackButtonPressed).toHaveBeenCalled();
});
test("typing in a text field is not a shortcut", async () => {
const user = userEvent.setup();
const toggleHandRaised = vi.fn();
const { getByRole } = render(
<>
<input type="text" aria-label="Name" />
<TestComponent toggleHandRaised={toggleHandRaised} />
</>,
);
getByRole("textbox", { name: "Name" }).focus();
await user.keyboard("h");
expect(toggleHandRaised).not.toHaveBeenCalled();
});
/**
* Element Call embedded in a host's page: the root is a container, with the
* rest of the page around it.
*/
const Embedded: FC<{ toggleHandRaised: () => void }> = ({
toggleHandRaised,
}) => {
const [root, setRoot] = useState<HTMLDivElement | null>(null);
return (
<>
<button>Host button</button>
<div ref={setRoot} data-testid="root">
{root !== null && (
<RootElementProvider value={root}>
<TestComponent toggleHandRaised={toggleHandRaised} />
</RootElementProvider>
)}
</div>
</>
);
};
test("only hears keys pressed within its root element", async () => {
const user = userEvent.setup();
const toggleHandRaised = vi.fn();
const { getByRole } = render(
<Embedded toggleHandRaised={toggleHandRaised} />,
);
// Focus on the host's page: none of Element Call's business
getByRole("button", { name: "Host button" }).focus();
await user.keyboard("h");
expect(toggleHandRaised).not.toHaveBeenCalled();
// Focus inside the root: a shortcut
getByRole("button", { name: "TEST" }).focus();
await user.keyboard("h");
expect(toggleHandRaised).toHaveBeenCalledOnce();
});