From 507663df87e750cb1ba5ebee2cdd3025fd0e3e15 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 5 Nov 2024 18:33:56 +0000 Subject: [PATCH] Add keyboard shortcuts for reaction sending. --- src/button/ReactionToggleButton.test.tsx | 94 +++++- src/button/ReactionToggleButton.tsx | 44 ++- .../ReactionToggleButton.test.tsx.snap | 317 +++++++++++++++++- 3 files changed, 425 insertions(+), 30 deletions(-) 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`] = ` +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +`;