11import 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
55import SendButton from './SendButton' ;
66import MicrophoneButton from './MicrophoneButton' ;
77import { AttachButton } from './AttachButton' ;
88import AttachMenu from '../AttachMenu' ;
99import StopButton from './StopButton' ;
10- import DOMPurify from 'dompurify ' ;
10+ import { ChatbotDisplayMode } from '../Chatbot ' ;
1111
1212export 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
6971export 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