Skip to content

Commit 2d8f19a

Browse files
authored
Merge pull request #438 from rebeccaalpert/textarea
Replace contenteditable div with PatternFly text area
2 parents 9cea659 + 9d12ef3 commit 2d8f19a

File tree

2 files changed

+179
-71
lines changed

2 files changed

+179
-71
lines changed

packages/module/src/MessageBar/MessageBar.scss

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,16 @@
4444

4545
&-input {
4646
flex: 1 1 auto;
47-
padding-block-start: var(--pf-chatbot__message-bar-child--PaddingBlockStart);
48-
padding-block-end: var(--pf-chatbot__message-bar-child--PaddingBlockEnd);
49-
overflow: hidden;
50-
position: relative;
51-
}
52-
53-
&-placeholder {
54-
position: absolute;
55-
top: 20px;
56-
left: 16px;
57-
color: var(--pf-t--global--text--color--placeholder);
58-
pointer-events: none;
59-
font-size: var(--pf-t--chatbot--font-size);
47+
padding-block-start: var(--pf-t--global--spacer--sm);
48+
padding-block-end: var(--pf-t--global--spacer--sm);
6049
}
6150
}
6251

6352
.pf-chatbot__message-textarea {
64-
padding-block-start: var(--pf-t--global--spacer--md);
65-
padding-block-end: var(--pf-t--global--spacer--md);
66-
padding-inline-start: var(--pf-t--global--spacer--md);
67-
padding-inline-end: var(--pf-t--global--spacer--md);
53+
--pf-v6-c-form-control--before--BorderStyle: none;
54+
--pf-v6-c-form-control--after--BorderStyle: none;
55+
resize: none;
56+
background-color: transparent;
6857
font-size: var(--pf-t--global--font--size--md);
6958
line-height: 1.5rem;
7059
max-height: 12rem;
@@ -75,5 +64,33 @@
7564
height: 100%;
7665
width: 100%;
7766
display: block !important;
78-
position: relative;
67+
68+
.pf-v6-c-form-control__textarea:focus-visible {
69+
outline: none;
70+
}
71+
textarea {
72+
outline-offset: 0px;
73+
--pf-v6-c-form-control--PaddingBlockStart: 0;
74+
--pf-v6-c-form-control--PaddingBlockEnd: 0;
75+
--pf-v6-c-form-control--BorderRadius: 0;
76+
}
77+
textarea:focus-visible {
78+
outline: none;
79+
}
80+
}
81+
82+
@media screen and (max-width: 359px) {
83+
.pf-chatbot__message-textarea {
84+
margin-top: var(--pf-t--global--spacer--md) !important;
85+
margin-bottom: var(--pf-t--global--spacer--md) !important;
86+
}
87+
}
88+
89+
.pf-chatbot--embedded {
90+
@media screen and (max-width: 415px) {
91+
.pf-chatbot__message-textarea {
92+
margin-top: var(--pf-t--global--spacer--md) !important;
93+
margin-bottom: var(--pf-t--global--spacer--md) !important;
94+
}
95+
}
7996
}

packages/module/src/MessageBar/MessageBar.tsx

Lines changed: 144 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from 'react';
2-
import { ButtonProps, DropEvent } from '@patternfly/react-core';
2+
import { ButtonProps, DropEvent, TextArea } from '@patternfly/react-core';
33

44
// Import Chatbot components
55
import SendButton from './SendButton';
66
import MicrophoneButton from './MicrophoneButton';
77
import { AttachButton } from './AttachButton';
88
import AttachMenu from '../AttachMenu';
99
import StopButton from './StopButton';
10-
import DOMPurify from 'dompurify';
10+
import { ChatbotDisplayMode } from '../Chatbot';
1111

1212
export interface MessageBarWithAttachMenuProps {
1313
/** Flag to enable whether attach menu is open */
@@ -63,7 +63,9 @@ export interface MessageBarProps {
6363
};
6464
};
6565
/** A callback for when the text area value changes. */
66-
onChange?: (event: React.ChangeEvent<HTMLDivElement>, value: string) => void;
66+
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>, value: string) => void;
67+
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
68+
displayMode?: ChatbotDisplayMode;
6769
}
6870

6971
export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
@@ -79,46 +81,148 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
7981
hasStopButton,
8082
buttonProps,
8183
onChange,
84+
displayMode,
8285
...props
8386
}: MessageBarProps) => {
8487
// Text Input
8588
// --------------------------------------------------------------------------
8689
const [message, setMessage] = React.useState<string>('');
8790
const [isListeningMessage, setIsListeningMessage] = React.useState<boolean>(false);
88-
const [showPlaceholder, setShowPlaceholder] = React.useState(true);
89-
const textareaRef = React.useRef<HTMLDivElement>(null);
91+
const [hasSentMessage, setHasSentMessage] = React.useState(false);
92+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
9093
const attachButtonRef = React.useRef<HTMLButtonElement>(null);
9194

92-
const handleInput = (event) => {
93-
// newMessage === '' doesn't work unless we trim, which causes other problems
94-
// textContent seems to work, but doesn't allow for markdown, so we need both
95-
const messageText = DOMPurify.sanitize(event.target.textContent);
96-
if (messageText === '') {
97-
setShowPlaceholder(true);
98-
setMessage('');
99-
onChange && onChange(event, '');
100-
} else {
101-
setShowPlaceholder(false);
102-
// this is so that tests work; RTL doesn't seem to like event.target.innerText, but browsers don't pick up markdown without it
103-
let newMessage = messageText;
104-
if (event.target.innerText) {
105-
newMessage = DOMPurify.sanitize(event.target.innerText);
95+
const setInitialLineHeight = (field: HTMLTextAreaElement) => {
96+
field.style.setProperty('line-height', '1rem');
97+
const parent = field.parentElement;
98+
if (parent) {
99+
parent.style.setProperty('margin-top', `1rem`);
100+
parent.style.setProperty('margin-bottom', `0rem`);
101+
parent.style.setProperty('height', 'inherit');
102+
103+
const grandparent = parent.parentElement;
104+
if (grandparent) {
105+
grandparent.style.setProperty('flex-basis', 'auto');
106106
}
107-
setMessage(newMessage);
108-
onChange && onChange(event, newMessage);
109107
}
110108
};
111109

112-
// Handle sending message
113-
const handleSend = React.useCallback(() => {
114-
onSendMessage(message);
110+
const setAutoHeight = (field: HTMLTextAreaElement) => {
111+
const parent = field.parentElement;
112+
if (parent) {
113+
parent.style.setProperty('height', 'inherit');
114+
const computed = window.getComputedStyle(field);
115+
// Calculate the height
116+
const height =
117+
parseInt(computed.getPropertyValue('border-top-width')) +
118+
parseInt(computed.getPropertyValue('padding-top')) +
119+
field.scrollHeight +
120+
parseInt(computed.getPropertyValue('padding-bottom')) +
121+
parseInt(computed.getPropertyValue('border-bottom-width'));
122+
parent.style.setProperty('height', `${height}px`);
123+
124+
if (height > 32 || window.innerWidth <= 507) {
125+
parent.style.setProperty('margin-bottom', `1rem`);
126+
parent.style.setProperty('margin-top', `1rem`);
127+
}
128+
}
129+
};
130+
131+
const textIsLongerThan2Lines = (field: HTMLTextAreaElement) => {
132+
const lineHeight = parseFloat(window.getComputedStyle(field).lineHeight);
133+
const lines = field.scrollHeight / lineHeight;
134+
return lines > 2;
135+
};
136+
137+
const setAutoWidth = (field: HTMLTextAreaElement) => {
138+
const parent = field.parentElement;
139+
if (parent) {
140+
const grandparent = parent.parentElement;
141+
if (textIsLongerThan2Lines(field) && grandparent) {
142+
grandparent.style.setProperty('flex-basis', `100%`);
143+
}
144+
}
145+
};
146+
147+
const handleNewLine = (field: HTMLTextAreaElement) => {
148+
const parent = field.parentElement;
149+
if (parent) {
150+
parent.style.setProperty('margin-bottom', `1rem`);
151+
parent.style.setProperty('margin-top', `1rem`);
152+
}
153+
};
154+
155+
React.useEffect(() => {
156+
const field = textareaRef.current;
157+
if (field) {
158+
if (field.value === '') {
159+
if (window.innerWidth > 507) {
160+
setInitialLineHeight(field);
161+
}
162+
} else {
163+
setAutoHeight(field);
164+
setAutoWidth(field);
165+
}
166+
}
167+
const resetHeight = () => {
168+
if (field) {
169+
if (field.value === '') {
170+
if (window.innerWidth > 507) {
171+
setInitialLineHeight(field);
172+
}
173+
} else {
174+
setAutoHeight(field);
175+
setAutoWidth(field);
176+
}
177+
}
178+
};
179+
window.addEventListener('resize', resetHeight);
180+
181+
return () => {
182+
window.removeEventListener('resize', resetHeight);
183+
};
184+
}, []);
185+
186+
React.useEffect(() => {
187+
const field = textareaRef.current;
188+
if (field) {
189+
if (field.value === '') {
190+
setInitialLineHeight(textareaRef.current);
191+
} else {
192+
setAutoHeight(textareaRef.current);
193+
setAutoWidth(field);
194+
}
195+
}
196+
}, [displayMode, message]);
197+
198+
React.useEffect(() => {
199+
const field = textareaRef.current;
200+
if (field) {
201+
setInitialLineHeight(field);
202+
setHasSentMessage(false);
203+
}
204+
}, [hasSentMessage]);
205+
206+
const handleChange = React.useCallback((event) => {
207+
onChange && onChange(event, event.target.value);
115208
if (textareaRef.current) {
116-
textareaRef.current.innerText = '';
117-
setShowPlaceholder(true);
118-
textareaRef.current.blur();
209+
if (event.target.value === '') {
210+
setInitialLineHeight(textareaRef.current);
211+
} else {
212+
setAutoHeight(textareaRef.current);
213+
}
119214
}
120-
setMessage('');
121-
}, [onSendMessage, message]);
215+
setMessage(event.target.value);
216+
}, []);
217+
218+
// Handle sending message
219+
const handleSend = React.useCallback(() => {
220+
setMessage((m) => {
221+
onSendMessage(m);
222+
setHasSentMessage(true);
223+
return '';
224+
});
225+
}, [onSendMessage]);
122226

123227
const handleKeyDown = React.useCallback(
124228
(event: React.KeyboardEvent) => {
@@ -128,6 +232,11 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
128232
handleSend();
129233
}
130234
}
235+
if (event.key === 'Enter' && event.shiftKey) {
236+
if (textareaRef.current) {
237+
handleNewLine(textareaRef.current);
238+
}
239+
}
131240
},
132241
[handleSend, isSendButtonDisabled, handleStopButton]
133242
);
@@ -139,12 +248,7 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
139248

140249
const handleSpeechRecognition = (message) => {
141250
setMessage(message);
142-
const textarea = textareaRef.current;
143-
if (textarea) {
144-
textarea.focus();
145-
textarea.textContent = DOMPurify.sanitize(message);
146-
}
147-
onChange && onChange({} as React.ChangeEvent<HTMLDivElement>, message);
251+
onChange && onChange({} as React.ChangeEvent<HTMLTextAreaElement>, message);
148252
};
149253

150254
const renderButtons = () => {
@@ -200,28 +304,15 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
200304
);
201305
};
202306

203-
const placeholder = isListeningMessage ? 'Listening' : 'Send a message...';
204-
205307
const messageBarContents = (
206308
<>
207309
<div className="pf-chatbot__message-bar-input">
208-
{(showPlaceholder || message === '') && (
209-
<div className="pf-chatbot__message-bar-placeholder">{placeholder}</div>
210-
)}
211-
<div
212-
contentEditable
213-
suppressContentEditableWarning={true}
214-
role="textbox"
215-
aria-multiline="false"
310+
<TextArea
216311
className="pf-chatbot__message-textarea"
217-
onInput={handleInput}
218-
onFocus={() => setShowPlaceholder(false)}
219-
onBlur={() => {
220-
if (message === '') {
221-
setShowPlaceholder(!showPlaceholder);
222-
}
223-
}}
224-
aria-label={placeholder}
312+
value={message}
313+
onChange={handleChange}
314+
aria-label={isListeningMessage ? 'Listening' : 'Send a message...'}
315+
placeholder={isListeningMessage ? 'Listening' : 'Send a message...'}
225316
ref={textareaRef}
226317
onKeyDown={handleKeyDown}
227318
{...props}

0 commit comments

Comments
 (0)