diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx
index 79c236d6..8d750e4f 100644
--- a/src/button/ReactionToggleButton.test.tsx
+++ b/src/button/ReactionToggleButton.test.tsx
@@ -1,15 +1,24 @@
-import { act, render } from "@testing-library/react";
+/*
+Copyright 2024 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only
+Please see LICENSE in the repository root for full details.
+*/
+
+import { act, fireEvent, render } from "@testing-library/react";
import { expect, test } from "vitest";
+import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
+import { TooltipProvider } from "@vector-im/compound-web";
+import { userEvent } from "@testing-library/user-event";
+import { ReactNode } from "react";
+
import {
MockRoom,
MockRTCSession,
TestReactionsWrapper,
} from "../utils/testReactions";
import { ReactionToggleButton } from "./ReactionToggleButton";
-import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
-import { TooltipProvider } from "@vector-im/compound-web";
import { ElementCallReactionEventType } from "../reactions";
-import { userEvent } from "@testing-library/user-event";
const memberUserIdAlice = "@alice:example.org";
const memberEventAlice = "$membership-alice:example.org";
@@ -24,20 +33,20 @@ function TestComponent({
}: {
rtcSession: MockRTCSession;
room: MockRoom;
-}) {
+}): ReactNode {
return (
+ />
);
}
-test("Can open menu", async () => {
+test("Can open menu", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, container } = render(
@@ -47,7 +56,7 @@ test("Can open menu", async () => {
expect(container).toMatchSnapshot();
});
-test("Can close menu", async () => {
+test("Can close menu", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, container } = render(
@@ -60,7 +69,7 @@ test("Can close menu", async () => {
expect(container).toMatchSnapshot();
});
-test("Can raise hand", async () => {
+test("Can raise hand", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, getByText, container } = render(
@@ -88,7 +97,7 @@ test("Can raise hand", async () => {
expect(container).toMatchSnapshot();
});
-test("Can can lower hand", async () => {
+test("Can can lower hand", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, getByText, container } = render(
@@ -105,7 +114,7 @@ test("Can can lower hand", async () => {
expect(container).toMatchSnapshot();
});
-test("Can react with emoji", async () => {
+test("Can react with emoji", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, getByText } = render(
@@ -172,7 +181,47 @@ test("Can search for and send emoji", async () => {
]);
});
-test("Can close search", async () => {
+test("Can search for and send emoji with the keyboard", async () => {
+ const user = userEvent.setup();
+ const room = new MockRoom(memberUserIdAlice);
+ const rtcSession = new MockRTCSession(room, membership);
+ const { getByText, getByRole, getByPlaceholderText, container } = render(
+ ,
+ );
+ act(() => {
+ getByRole("button").click();
+ });
+ act(() => {
+ getByRole("button", {
+ name: "Search",
+ }).click();
+ });
+ const searchField = getByPlaceholderText("Search reactions…");
+ await act(async () => {
+ searchField.focus();
+ await user.keyboard("crickets");
+ });
+ expect(container).toMatchSnapshot();
+ act(() => {
+ fireEvent.keyDown(searchField, { key: "Enter" });
+ });
+ expect(room.testSentEvents).toEqual([
+ [
+ undefined,
+ ElementCallReactionEventType,
+ {
+ "m.relates_to": {
+ event_id: memberEventAlice,
+ rel_type: "m.reference",
+ },
+ name: "crickets",
+ emoji: "🦗",
+ },
+ ],
+ ]);
+});
+
+test("Can close search", () => {
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole, container } = render(
@@ -193,3 +242,24 @@ test("Can close search", async () => {
});
expect(container).toMatchSnapshot();
});
+
+test("Can close search with the escape key", () => {
+ const room = new MockRoom(memberUserIdAlice);
+ const rtcSession = new MockRTCSession(room, membership);
+ const { getByRole, container, getByPlaceholderText } = render(
+ ,
+ );
+ act(() => {
+ getByRole("button").click();
+ });
+ act(() => {
+ getByRole("button", {
+ name: "Search",
+ }).click();
+ });
+ const searchField = getByPlaceholderText("Search reactions…");
+ act(() => {
+ fireEvent.keyDown(searchField, { key: "Escape" });
+ });
+ expect(container).toMatchSnapshot();
+});
diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx
index 3d77b9c5..fbfe1d36 100644
--- a/src/button/ReactionToggleButton.tsx
+++ b/src/button/ReactionToggleButton.tsx
@@ -21,8 +21,12 @@ import {
ChangeEventHandler,
ComponentPropsWithoutRef,
FC,
+ FormEventHandler,
+ KeyboardEvent,
+ KeyboardEventHandler,
ReactNode,
useCallback,
+ useEffect,
useMemo,
useState,
} from "react";
@@ -31,6 +35,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { EventType, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
+import classNames from "classnames";
import { useReactions } from "../useReactions";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
@@ -40,7 +45,6 @@ import {
ReactionSet,
ElementCallReactionEventType,
} from "../reactions";
-import classNames from "classnames";
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
raised: boolean;
@@ -64,12 +68,12 @@ const InnerButton: FC = ({ raised, ...props }) => {
};
export function ReactionPopupMenu({
- sendRelation,
+ sendReaction,
toggleRaisedHand,
isHandRaised,
canReact,
}: {
- sendRelation: (reaction: ReactionOption) => void;
+ sendReaction: (reaction: ReactionOption) => void;
toggleRaisedHand: () => void;
isHandRaised: boolean;
canReact: boolean;
@@ -94,6 +98,26 @@ export function ReactionPopupMenu({
[searchText, isSearching],
);
+ const onSearchKeyDown = useCallback>(
+ (ev) => {
+ if (ev.key === "Enter") {
+ ev.preventDefault();
+ if (!canReact) {
+ return;
+ }
+ if (filteredReactionSet.length !== 1) {
+ return;
+ }
+ sendReaction(filteredReactionSet[0]);
+ setIsSearching(false);
+ } else if (ev.key === "Escape") {
+ ev.preventDefault();
+ setIsSearching(false);
+ }
+ },
+ [sendReaction, filteredReactionSet, canReact, setIsSearching],
+ );
+
return (
@@ -114,15 +138,17 @@ export function ReactionPopupMenu({
{isSearching ? (
<>
- e.preventDefault()}
- >
+
sendRelation(reaction)}
+ onClick={() => sendReaction(reaction)}
>
{reaction.emoji}
@@ -287,7 +313,7 @@ export function ReactionToggleButton({
void sendRelation(reaction)}
+ sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={toggleRaisedHand}
/>
)}
diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap
index 45811dc0..2551846e 100644
--- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap
+++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap
@@ -204,7 +204,7 @@ exports[`Can close search 1`] = `
+
+
+`;
+
+exports[`Can close search with the escape key 1`] = `
+
`;
+
+exports[`Can search for and send emoji with the keyboard 1`] = `
+
+`;