diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx
index 300417f66d..8701f5be77 100644
--- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx
@@ -15,15 +15,16 @@ limitations under the License.
*/
import React, { useCallback, useEffect } from 'react';
-import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
-import { useRoomContext } from '../../../../contexts/RoomContext';
-import { sendMessage } from './message';
-import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
-import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
-import { FormattingButtons } from './FormattingButtons';
import { Editor } from './Editor';
+import { FormattingButtons } from './FormattingButtons';
+import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
+import { sendMessage } from './message';
+import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
+import { useRoomContext } from '../../../../contexts/RoomContext';
+import { useWysiwygActionHandler } from './useWysiwygActionHandler';
interface WysiwygProps {
disabled?: boolean;
@@ -55,6 +56,8 @@ export function WysiwygComposer(
ref.current?.focus();
}, [content, mxClient, roomContext, wysiwyg, props, ref]);
+ useWysiwygActionHandler(disabled, ref);
+
return (
diff --git a/src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts
new file mode 100644
index 0000000000..683498d485
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts
@@ -0,0 +1,73 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useRef } from "react";
+
+import defaultDispatcher from "../../../../dispatcher/dispatcher";
+import { Action } from "../../../../dispatcher/actions";
+import { ActionPayload } from "../../../../dispatcher/payloads";
+import { IRoomState } from "../../../structures/RoomView";
+import { TimelineRenderingType, useRoomContext } from "../../../../contexts/RoomContext";
+import { useDispatcher } from "../../../../hooks/useDispatcher";
+
+export function useWysiwygActionHandler(
+ disabled: boolean,
+ composerElement: React.MutableRefObject,
+) {
+ const roomContext = useRoomContext();
+ const timeoutId = useRef();
+
+ useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
+ // don't let the user into the composer if it is disabled - all of these branches lead
+ // to the cursor being in the composer
+ if (disabled) return;
+
+ const context = payload.context ?? TimelineRenderingType.Room;
+
+ switch (payload.action) {
+ case "reply_to_event":
+ case Action.FocusSendMessageComposer:
+ focusComposer(composerElement, context, roomContext, timeoutId);
+ break;
+ // TODO: case Action.ComposerInsert: - see SendMessageComposer
+ }
+ });
+}
+
+function focusComposer(
+ composerElement: React.MutableRefObject,
+ renderingType: TimelineRenderingType,
+ roomContext: IRoomState,
+ timeoutId: React.MutableRefObject,
+) {
+ if (renderingType === roomContext.timelineRenderingType) {
+ // Immediately set the focus, so if you start typing it
+ // will appear in the composer
+ composerElement.current?.focus();
+ // If we call focus immediate, the focus _is_ in the right
+ // place, but the cursor is invisible, presumably because
+ // some other event is still processing.
+ // The following line ensures that the cursor is actually
+ // visible in composer.
+ if (timeoutId.current) {
+ clearTimeout(timeoutId.current);
+ }
+ timeoutId.current = setTimeout(
+ () => composerElement.current?.focus(),
+ 200,
+ );
+ }
+}
diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx
index d1d4596f5a..b0aa838879 100644
--- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx
@@ -14,15 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import "@testing-library/jest-dom";
import React from "react";
-import { act, render, screen } from "@testing-library/react";
+import { act, render, screen, waitFor } from "@testing-library/react";
-import { IRoomState } from "../../../../../src/components/structures/RoomView";
-import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
-import { Layout } from "../../../../../src/settings/enums/Layout";
-import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
+import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
+import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
+import { Action } from "../../../../../src/dispatcher/actions";
+import { IRoomState } from "../../../../../src/components/structures/RoomView";
+import { Layout } from "../../../../../src/settings/enums/Layout";
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
+import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
@@ -92,7 +95,7 @@ describe('WysiwygComposer', () => {
};
let sendMessage: () => void;
- const customRender = (onChange = (content: string) => void 0, disabled = false) => {
+ const customRender = (onChange = (_content: string) => void 0, disabled = false) => {
return render(
@@ -140,5 +143,58 @@ describe('WysiwygComposer', () => {
expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent);
expect(screen.getByRole('textbox')).toHaveFocus();
});
+
+ it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
+ // Given we don't have focus
+ customRender(() => {}, false);
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ });
+
+ it('Should focus when receiving a reply_to_event action', async () => {
+ // Given we don't have focus
+ customRender(() => {}, false);
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: "reply_to_event",
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ });
+
+ it('Should not focus when disabled', async () => {
+ // Given we don't have focus and we are disabled
+ customRender(() => {}, true);
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+
+ // When we send an action that would cause us to get focus
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+ // (Send a second event to exercise the clearTimeout logic)
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+
+ // Wait for event dispatch to happen
+ await new Promise((r) => setTimeout(r, 200));
+
+ // Then we don't get it because we are disabled
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+ });
});