diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx
index 64c940c2fd..e15b5ef57f 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx
@@ -43,7 +43,13 @@ export function PlainTextComposer({
usePlainTextInitialization(initialContent, ref);
useSetCursorPosition(disabled, ref);
- return
+ return
{ children?.(ref, composerFunctions) }
;
diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
index 73125a910a..974e89f0ce 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
@@ -52,7 +52,7 @@ export const WysiwygComposer = memo(function WysiwygComposer(
useSetCursorPosition(!isReady, ref);
return (
-
+
{ children?.(ref, wysiwyg) }
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts
index dcaacd98ea..abf2a6a6d2 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts
@@ -16,13 +16,10 @@ limitations under the License.
import { RefObject, useEffect } from "react";
-import { setCursorPositionAtTheEnd } from "./utils";
-
export function usePlainTextInitialization(initialContent: string, ref: RefObject
) {
useEffect(() => {
if (ref.current) {
ref.current.innerText = initialContent;
- setCursorPositionAtTheEnd(ref.current);
}
}, [ref, initialContent]);
}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts
index 98d09c12b0..02063ddcfb 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts
@@ -16,7 +16,7 @@ limitations under the License.
import { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react";
-import { useInputEventProcessor } from "./useInputEventProcessor";
+import { useSettingValue } from "../../../../../hooks/useSettings";
function isDivElement(target: EventTarget): target is HTMLDivElement {
return target instanceof HTMLDivElement;
@@ -26,25 +26,25 @@ export function usePlainTextListeners(onChange: (content: string) => void, onSen
const ref = useRef();
const send = useCallback((() => {
if (ref.current) {
- ref.current.innerText = '';
+ ref.current.innerHTML = '';
}
onSend();
}), [ref, onSend]);
- const inputEventProcessor = useInputEventProcessor(send);
-
const onInput = useCallback((event: SyntheticEvent) => {
if (isDivElement(event.target)) {
- onChange(event.target.innerText);
+ onChange(event.target.innerHTML);
}
- inputEventProcessor(event.nativeEvent);
- }, [onChange, inputEventProcessor]);
+ }, [onChange]);
+ const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend");
const onKeyDown = useCallback((event: KeyboardEvent) => {
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
+ event.preventDefault();
+ event.stopPropagation();
send();
}
- }, [send]);
+ }, [isCtrlEnter, send]);
return { ref, onInput, onPaste: onInput, onKeyDown };
}
diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
index 573ae451be..c2e1b8a1fb 100644
--- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
@@ -26,6 +26,8 @@ import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
+import * as useComposerFunctions
+ from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
const mockClear = jest.fn();
@@ -68,86 +70,119 @@ describe('SendWysiwygComposer', () => {
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
- const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => {
+ const customRender = (
+ onChange = (_content: string) => void 0,
+ onSend = () => void 0,
+ disabled = false,
+ isRichTextEnabled = true) => {
return render(
-
+
,
);
};
- it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- screen.getByLabelText('Bold').focus();
- expect(screen.getByRole('textbox')).not.toHaveFocus();
+ it('Should render WysiwygComposer when isRichTextEnabled is at true', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false, true);
- // 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());
+ // Then
+ expect(screen.getByTestId('WysiwygComposer')).toBeTruthy();
});
- it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- screen.getByLabelText('Bold').focus();
- expect(screen.getByRole('textbox')).not.toHaveFocus();
+ it('Should render PlainTextComposer when isRichTextEnabled is at false', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false, false);
- // When we send the right action
- defaultDispatcher.dispatch({
- action: Action.ClearAndFocusSendMessageComposer,
- context: null,
- });
-
- // Then the component gets the focus
- await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
- expect(mockClear).toBeCalledTimes(1);
+ // Then
+ expect(screen.getByTestId('PlainTextComposer')).toBeTruthy();
});
- it('Should focus when receiving a reply_to_event action', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- screen.getByLabelText('Bold').focus();
- expect(screen.getByRole('textbox')).not.toHaveFocus();
+ describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])(
+ 'Should focus when receiving an Action.FocusSendMessageComposer action',
+ ({ isRichTextEnabled }) => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
- // When we send the right action
- defaultDispatcher.dispatch({
- action: "reply_to_event",
- context: null,
+ it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
+ // Given we don't have focus
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+ document.head.focus();
+ // screen.getByLabelText('Bold').focus();
+ // 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 and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
+ // Given we don't have focus
+ const mock = jest.spyOn(useComposerFunctions, 'useComposerFunctions');
+ mock.mockReturnValue({ clear: mockClear });
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+ // screen.getByLabelText('Bold').focus();
+ // expect(screen.getByRole('textbox')).not.toHaveFocus();
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: Action.ClearAndFocusSendMessageComposer,
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ expect(mockClear).toBeCalledTimes(1);
+
+ mock.mockRestore();
+ });
+
+ it('Should focus when receiving a reply_to_event action', async () => {
+ // Given we don't have focus
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+ // screen.getByLabelText('Bold').focus();
+ // 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(jest.fn(), jest.fn(), true, isRichTextEnabled);
+ 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();
+ });
});
-
- // 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(jest.fn(), jest.fn(), 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();
- });
});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
new file mode 100644
index 0000000000..5d1b03020c
--- /dev/null
+++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
@@ -0,0 +1,94 @@
+/*
+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 React from 'react';
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import { PlainTextComposer }
+ from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
+
+// Work around missing ClipboardEvent type
+class MyClipboardEvent {}
+window.ClipboardEvent = MyClipboardEvent as any;
+
+describe('PlainTextComposer', () => {
+ const customRender = (
+ onChange = (_content: string) => void 0,
+ onSend = () => void 0,
+ disabled = false,
+ initialContent?: string) => {
+ return render(
+ ,
+ );
+ };
+
+ it('Should have contentEditable at false when disabled', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), true);
+
+ // Then
+ expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
+ });
+
+ it('Should have focus', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false);
+
+ // Then
+ expect(screen.getByRole('textbox')).toHaveFocus();
+ });
+
+ it('Should call onChange handler', async () => {
+ // When
+ const content = 'content';
+ const onChange = jest.fn();
+ customRender(onChange, jest.fn());
+ await userEvent.type(screen.getByRole('textbox'), content);
+
+ // Then
+ expect(onChange).toBeCalledWith(content);
+ });
+
+ it('Should call onSend when Enter is pressed', async () => {
+ //When
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ await userEvent.type(screen.getByRole('textbox'), '{enter}');
+
+ // Then it sends a message
+ expect(onSend).toBeCalledTimes(1);
+ });
+
+ it('Should clear textbox content when clear is called', async () => {
+ //When
+ let composer;
+ render(
+
+ { (ref, composerFunctions) => {
+ composer = composerFunctions;
+ return null;
+ } }
+ ,
+ );
+ await userEvent.type(screen.getByRole('textbox'), 'content');
+ expect(screen.getByRole('textbox').innerHTML).toBe('content');
+ composer.clear();
+
+ // Then
+ expect(screen.getByRole('textbox').innerHTML).toBeFalsy();
+ });
+});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
index cb9c37071c..7e3db04abc 100644
--- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
@@ -19,10 +19,6 @@ import React from "react";
import { render, screen } from "@testing-library/react";
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
-import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
-import { IRoomState } from "../../../../../../src/components/structures/RoomView";
-import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils";
-import RoomContext from "../../../../../../src/contexts/RoomContext";
import { WysiwygComposer }
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
@@ -54,36 +50,14 @@ jest.mock("@matrix-org/matrix-wysiwyg", () => ({
}));
describe('WysiwygComposer', () => {
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- const mockClient = createTestClient();
- const mockEvent = mkEvent({
- type: "m.room.message",
- room: 'myfakeroom',
- user: 'myfakeuser',
- content: { "msgtype": "m.text", "body": "Replying to this" },
- event: true,
- });
- const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any;
- mockRoom.findEventById = jest.fn(eventId => {
- return eventId === mockEvent.getId() ? mockEvent : null;
- });
-
- const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
-
const customRender = (
onChange = (_content: string) => void 0,
onSend = () => void 0,
disabled = false,
initialContent?: string) => {
return render(
-
-
-
-
- ,
+ ,
+
);
};