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

Commit 7c63d52

Browse files
authored
Add placeholder for rich text editor (#9613)
* Add placeholder for rich text editor
1 parent 8b8d24c commit 7c63d52

File tree

8 files changed

+109
-19
lines changed

8 files changed

+109
-19
lines changed

res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss

+11
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,15 @@ limitations under the License.
3232
user-select: all;
3333
}
3434
}
35+
36+
.mx_WysiwygComposer_Editor_content_placeholder::before {
37+
content: var(--placeholder);
38+
width: 0;
39+
height: 0;
40+
overflow: visible;
41+
display: inline-block;
42+
pointer-events: none;
43+
white-space: nowrap;
44+
color: $tertiary-content;
45+
}
3546
}

src/components/views/rooms/MessageComposer.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
458458
initialContent={this.state.initialComposerContent}
459459
e2eStatus={this.props.e2eStatus}
460460
menuPosition={menuPosition}
461+
placeholder={this.renderPlaceholderText()}
461462
/>;
462463
} else {
463464
composer =

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

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const Content = forwardRef<HTMLElement, ContentProps>(
4343
interface SendWysiwygComposerProps {
4444
initialContent?: string;
4545
isRichTextEnabled: boolean;
46+
placeholder?: string;
4647
disabled?: boolean;
4748
e2eStatus?: E2EStatus;
4849
onChange: (content: string) => void;

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

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

17-
import React, { forwardRef, memo, MutableRefObject, ReactNode } from 'react';
17+
import classNames from 'classnames';
18+
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react';
1819

1920
import { useIsExpanded } from '../hooks/useIsExpanded';
2021

2122
const HEIGHT_BREAKING_POINT = 20;
2223

2324
interface EditorProps {
2425
disabled: boolean;
26+
placeholder?: string;
2527
leftComponent?: ReactNode;
2628
rightComponent?: ReactNode;
2729
}
2830

2931
export const Editor = memo(
3032
forwardRef<HTMLDivElement, EditorProps>(
31-
function Editor({ disabled, leftComponent, rightComponent }: EditorProps, ref,
33+
function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref,
3234
) {
3335
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
3436

@@ -39,15 +41,20 @@ export const Editor = memo(
3941
>
4042
{ leftComponent }
4143
<div className="mx_WysiwygComposer_Editor_container">
42-
<div className="mx_WysiwygComposer_Editor_content"
43-
ref={ref}
44-
contentEditable={!disabled}
45-
role="textbox"
46-
aria-multiline="true"
47-
aria-autocomplete="list"
48-
aria-haspopup="listbox"
49-
dir="auto"
50-
aria-disabled={disabled}
44+
<div className={classNames("mx_WysiwygComposer_Editor_content",
45+
{
46+
"mx_WysiwygComposer_Editor_content_placeholder": Boolean(placeholder),
47+
},
48+
)}
49+
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
50+
ref={ref}
51+
contentEditable={!disabled}
52+
role="textbox"
53+
aria-multiline="true"
54+
aria-autocomplete="list"
55+
aria-haspopup="listbox"
56+
dir="auto"
57+
aria-disabled={disabled}
5158
/>
5259
</div>
5360
{ rightComponent }

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface PlainTextComposerProps {
2929
disabled?: boolean;
3030
onChange?: (content: string) => void;
3131
onSend?: () => void;
32+
placeholder?: string;
3233
initialContent?: string;
3334
className?: string;
3435
leftComponent?: ReactNode;
@@ -45,16 +46,18 @@ export function PlainTextComposer({
4546
onSend,
4647
onChange,
4748
children,
49+
placeholder,
4850
initialContent,
4951
leftComponent,
5052
rightComponent,
5153
}: PlainTextComposerProps,
5254
) {
53-
const { ref, onInput, onPaste, onKeyDown } = usePlainTextListeners(onChange, onSend);
55+
const { ref, onInput, onPaste, onKeyDown, content } = usePlainTextListeners(initialContent, onChange, onSend);
5456
const composerFunctions = useComposerFunctions(ref);
5557
usePlainTextInitialization(initialContent, ref);
5658
useSetCursorPosition(disabled, ref);
5759
const { isFocused, onFocus } = useIsFocused();
60+
const computedPlaceholder = !content && placeholder || undefined;
5861

5962
return <div
6063
data-testid="PlainTextComposer"
@@ -65,7 +68,7 @@ export function PlainTextComposer({
6568
onPaste={onPaste}
6669
onKeyDown={onKeyDown}
6770
>
68-
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} />
71+
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
6972
{ children?.(ref, composerFunctions) }
7073
</div>;
7174
}

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface WysiwygComposerProps {
2828
disabled?: boolean;
2929
onChange?: (content: string) => void;
3030
onSend: () => void;
31+
placeholder?: string;
3132
initialContent?: string;
3233
className?: string;
3334
leftComponent?: ReactNode;
@@ -43,6 +44,7 @@ export const WysiwygComposer = memo(function WysiwygComposer(
4344
disabled = false,
4445
onChange,
4546
onSend,
47+
placeholder,
4648
initialContent,
4749
className,
4850
leftComponent,
@@ -65,11 +67,12 @@ export const WysiwygComposer = memo(function WysiwygComposer(
6567
useSetCursorPosition(!isReady, ref);
6668

6769
const { isFocused, onFocus } = useIsFocused();
70+
const computedPlaceholder = !content && placeholder || undefined;
6871

6972
return (
7073
<div data-testid="WysiwygComposer" className={classNames(className, { [`${className}-focused`]: isFocused })} onFocus={onFocus} onBlur={onFocus}>
7174
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
72-
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} />
75+
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
7376
{ children?.(ref, wysiwyg) }
7477
</div>
7578
);

src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,21 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react";
17+
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
1818

1919
import { useSettingValue } from "../../../../../hooks/useSettings";
2020

2121
function isDivElement(target: EventTarget): target is HTMLDivElement {
2222
return target instanceof HTMLDivElement;
2323
}
2424

25-
export function usePlainTextListeners(onChange?: (content: string) => void, onSend?: () => void) {
25+
export function usePlainTextListeners(
26+
initialContent?: string,
27+
onChange?: (content: string) => void,
28+
onSend?: () => void,
29+
) {
2630
const ref = useRef<HTMLDivElement | null>(null);
31+
const [content, setContent] = useState<string | undefined>(initialContent);
2732
const send = useCallback((() => {
2833
if (ref.current) {
2934
ref.current.innerHTML = '';
@@ -33,6 +38,7 @@ export function usePlainTextListeners(onChange?: (content: string) => void, onSe
3338

3439
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
3540
if (isDivElement(event.target)) {
41+
setContent(event.target.innerHTML);
3642
onChange?.(event.target.innerHTML);
3743
}
3844
}, [onChange]);
@@ -46,5 +52,5 @@ export function usePlainTextListeners(onChange?: (content: string) => void, onSe
4652
}
4753
}, [isCtrlEnter, send]);
4854

49-
return { ref, onInput, onPaste: onInput, onKeyDown };
55+
return { ref, onInput, onPaste: onInput, onKeyDown, content };
5056
}

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

+60-2
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ describe('SendWysiwygComposer', () => {
5151
onChange = (_content: string) => void 0,
5252
onSend = () => void 0,
5353
disabled = false,
54-
isRichTextEnabled = true) => {
54+
isRichTextEnabled = true,
55+
placeholder?: string) => {
5556
return render(
5657
<MatrixClientContext.Provider value={mockClient}>
5758
<RoomContext.Provider value={defaultRoomContext}>
58-
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} isRichTextEnabled={isRichTextEnabled} menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} />
59+
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} isRichTextEnabled={isRichTextEnabled} menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} placeholder={placeholder} />
5960
</RoomContext.Provider>
6061
</MatrixClientContext.Provider>,
6162
);
@@ -164,5 +165,62 @@ describe('SendWysiwygComposer', () => {
164165
expect(screen.getByRole('textbox')).not.toHaveFocus();
165166
});
166167
});
168+
169+
describe.each([
170+
{ isRichTextEnabled: true },
171+
{ isRichTextEnabled: false },
172+
])('Placeholder when %s',
173+
({ isRichTextEnabled }) => {
174+
afterEach(() => {
175+
jest.resetAllMocks();
176+
});
177+
178+
it('Should not has placeholder', async () => {
179+
// When
180+
console.log('here');
181+
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
182+
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
183+
184+
// Then
185+
expect(screen.getByRole('textbox')).not.toHaveClass("mx_WysiwygComposer_Editor_content_placeholder");
186+
});
187+
188+
it('Should has placeholder', async () => {
189+
// When
190+
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, 'my placeholder');
191+
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
192+
193+
// Then
194+
expect(screen.getByRole('textbox')).toHaveClass("mx_WysiwygComposer_Editor_content_placeholder");
195+
});
196+
197+
it('Should display or not placeholder when editor content change', async () => {
198+
// When
199+
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, 'my placeholder');
200+
await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true"));
201+
screen.getByRole('textbox').innerHTML = 'f';
202+
fireEvent.input(screen.getByRole('textbox'), {
203+
data: 'f',
204+
inputType: 'insertText',
205+
});
206+
207+
// Then
208+
await waitFor(() =>
209+
expect(screen.getByRole('textbox'))
210+
.not.toHaveClass("mx_WysiwygComposer_Editor_content_placeholder"),
211+
);
212+
213+
// When
214+
screen.getByRole('textbox').innerHTML = '';
215+
fireEvent.input(screen.getByRole('textbox'), {
216+
inputType: 'deleteContentBackward',
217+
});
218+
219+
// Then
220+
await waitFor(() =>
221+
expect(screen.getByRole('textbox')).toHaveClass("mx_WysiwygComposer_Editor_content_placeholder"),
222+
);
223+
});
224+
});
167225
});
168226

0 commit comments

Comments
 (0)