Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e38c9e0

Browse files
authored
Automatically focus the WYSIWYG composer when you enter a room (#9412)
Automatically focus the WYSIWYG composer when you enter a room
1 parent 13e9e14 commit e38c9e0

File tree

3 files changed

+142
-10
lines changed

3 files changed

+142
-10
lines changed

src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ limitations under the License.
1515
*/
1616

1717
import React, { useCallback, useEffect } from 'react';
18-
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
1918
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
19+
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
2020

21-
import { useRoomContext } from '../../../../contexts/RoomContext';
22-
import { sendMessage } from './message';
21+
import { Editor } from './Editor';
22+
import { FormattingButtons } from './FormattingButtons';
2323
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
24+
import { sendMessage } from './message';
2425
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
25-
import { FormattingButtons } from './FormattingButtons';
26-
import { Editor } from './Editor';
26+
import { useRoomContext } from '../../../../contexts/RoomContext';
27+
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
2728

2829
interface WysiwygProps {
2930
disabled?: boolean;
@@ -55,6 +56,8 @@ export function WysiwygComposer(
5556
ref.current?.focus();
5657
}, [content, mxClient, roomContext, wysiwyg, props, ref]);
5758

59+
useWysiwygActionHandler(disabled, ref);
60+
5861
return (
5962
<div className="mx_WysiwygComposer">
6063
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { useRef } from "react";
18+
19+
import defaultDispatcher from "../../../../dispatcher/dispatcher";
20+
import { Action } from "../../../../dispatcher/actions";
21+
import { ActionPayload } from "../../../../dispatcher/payloads";
22+
import { IRoomState } from "../../../structures/RoomView";
23+
import { TimelineRenderingType, useRoomContext } from "../../../../contexts/RoomContext";
24+
import { useDispatcher } from "../../../../hooks/useDispatcher";
25+
26+
export function useWysiwygActionHandler(
27+
disabled: boolean,
28+
composerElement: React.MutableRefObject<HTMLElement>,
29+
) {
30+
const roomContext = useRoomContext();
31+
const timeoutId = useRef<number>();
32+
33+
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
34+
// don't let the user into the composer if it is disabled - all of these branches lead
35+
// to the cursor being in the composer
36+
if (disabled) return;
37+
38+
const context = payload.context ?? TimelineRenderingType.Room;
39+
40+
switch (payload.action) {
41+
case "reply_to_event":
42+
case Action.FocusSendMessageComposer:
43+
focusComposer(composerElement, context, roomContext, timeoutId);
44+
break;
45+
// TODO: case Action.ComposerInsert: - see SendMessageComposer
46+
}
47+
});
48+
}
49+
50+
function focusComposer(
51+
composerElement: React.MutableRefObject<HTMLElement>,
52+
renderingType: TimelineRenderingType,
53+
roomContext: IRoomState,
54+
timeoutId: React.MutableRefObject<number>,
55+
) {
56+
if (renderingType === roomContext.timelineRenderingType) {
57+
// Immediately set the focus, so if you start typing it
58+
// will appear in the composer
59+
composerElement.current?.focus();
60+
// If we call focus immediate, the focus _is_ in the right
61+
// place, but the cursor is invisible, presumably because
62+
// some other event is still processing.
63+
// The following line ensures that the cursor is actually
64+
// visible in composer.
65+
if (timeoutId.current) {
66+
clearTimeout(timeoutId.current);
67+
}
68+
timeoutId.current = setTimeout(
69+
() => composerElement.current?.focus(),
70+
200,
71+
);
72+
}
73+
}

test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx

+61-5
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import "@testing-library/jest-dom";
1718
import React from "react";
18-
import { act, render, screen } from "@testing-library/react";
19+
import { act, render, screen, waitFor } from "@testing-library/react";
1920

20-
import { IRoomState } from "../../../../../src/components/structures/RoomView";
21+
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
2122
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
23+
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
24+
import { Action } from "../../../../../src/dispatcher/actions";
25+
import { IRoomState } from "../../../../../src/components/structures/RoomView";
2226
import { Layout } from "../../../../../src/settings/enums/Layout";
23-
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
24-
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
2527
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
28+
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
2629

2730
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
2831
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
@@ -92,7 +95,7 @@ describe('WysiwygComposer', () => {
9295
};
9396

9497
let sendMessage: () => void;
95-
const customRender = (onChange = (content: string) => void 0, disabled = false) => {
98+
const customRender = (onChange = (_content: string) => void 0, disabled = false) => {
9699
return render(
97100
<MatrixClientContext.Provider value={mockClient}>
98101
<RoomContext.Provider value={defaultRoomContext}>
@@ -140,5 +143,58 @@ describe('WysiwygComposer', () => {
140143
expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent);
141144
expect(screen.getByRole('textbox')).toHaveFocus();
142145
});
146+
147+
it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
148+
// Given we don't have focus
149+
customRender(() => {}, false);
150+
expect(screen.getByRole('textbox')).not.toHaveFocus();
151+
152+
// When we send the right action
153+
defaultDispatcher.dispatch({
154+
action: Action.FocusSendMessageComposer,
155+
context: null,
156+
});
157+
158+
// Then the component gets the focus
159+
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
160+
});
161+
162+
it('Should focus when receiving a reply_to_event action', async () => {
163+
// Given we don't have focus
164+
customRender(() => {}, false);
165+
expect(screen.getByRole('textbox')).not.toHaveFocus();
166+
167+
// When we send the right action
168+
defaultDispatcher.dispatch({
169+
action: "reply_to_event",
170+
context: null,
171+
});
172+
173+
// Then the component gets the focus
174+
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
175+
});
176+
177+
it('Should not focus when disabled', async () => {
178+
// Given we don't have focus and we are disabled
179+
customRender(() => {}, true);
180+
expect(screen.getByRole('textbox')).not.toHaveFocus();
181+
182+
// When we send an action that would cause us to get focus
183+
defaultDispatcher.dispatch({
184+
action: Action.FocusSendMessageComposer,
185+
context: null,
186+
});
187+
// (Send a second event to exercise the clearTimeout logic)
188+
defaultDispatcher.dispatch({
189+
action: Action.FocusSendMessageComposer,
190+
context: null,
191+
});
192+
193+
// Wait for event dispatch to happen
194+
await new Promise((r) => setTimeout(r, 200));
195+
196+
// Then we don't get it because we are disabled
197+
expect(screen.getByRole('textbox')).not.toHaveFocus();
198+
});
143199
});
144200

0 commit comments

Comments
 (0)