Skip to content

Commit

Permalink
Merge pull request #319 from rebeccaalpert/contenteditable
Browse files Browse the repository at this point in the history
fix(MessageBar): Swap out textarea
  • Loading branch information
nicolethoen authored Nov 19, 2024
2 parents bcd4c46 + 5b75285 commit 8b55ef1
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 33 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"whatwg-fetch": "^3.6.20"
},
"dependencies": {
"dompurify": "^3.2.0",
"react-dropzone": "^14.2.3"
},
"packageManager": "[email protected]+sha512.837566d24eec14ec0f5f1411adb544e892b3454255e61fdef8fd05f3429480102806bac7446bc9daff3896b01ae4b62d00096c7e989f1596f2af10b927532f39"
Expand Down
32 changes: 21 additions & 11 deletions packages/module/src/MessageBar/MessageBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@

display: flex;
flex-wrap: wrap;
align-items: flex-end;
align-items: center;
justify-content: flex-end;
background-color: var(--pf-t--global--background--color--primary--default);
border-radius: calc(var(--pf-t--global--border--radius--medium) * 2);
transition: box-shadow var(--pf-t--chatbot--timing-function) var(--pf-t--global--motion--duration--sm);

overflow: hidden;

&:hover {
box-shadow: inset 0 0 0 1px var(--pf-t--global--border--color--default);
}
Expand All @@ -34,34 +36,42 @@
&-actions {
display: flex;
justify-content: end;
padding-block-start: var(--pf-chatbot__message-bar-child--PaddingBlockStart);
padding-block-end: var(--pf-t--global--spacer--sm);
padding-block-start: var(--pf-t--global--spacer--xs);
padding-block-end: var(--pf-t--global--spacer--xs);
gap: var(--pf-t--global--spacer--xs);
}

&-input {
flex: 1 1 auto;
padding-block-start: var(--pf-chatbot__message-bar-child--PaddingBlockStart);
padding-block-end: var(--pf-chatbot__message-bar-child--PaddingBlockEnd);
overflow: hidden;
position: relative;
}

&-placeholder {
position: absolute;
top: 20px;
left: 16px;
color: var(--pf-t--global--text--color--placeholder);
pointer-events: none;
}
}

// - Form control textarea
.pf-chatbot__message-textarea {
padding-block-start: var(--pf-t--global--spacer--md);
padding-block-end: var(--pf-t--global--spacer--md);
padding-inline-start: var(--pf-t--global--spacer--md);
padding-inline-end: var(--pf-t--global--spacer--md);
--pf-v6-c-form-control--before--BorderStyle: none;
--pf-v6-c-form-control--after--BorderStyle: none;
background-color: transparent;
font-size: var(--pf-t--global--font--size--md);
line-height: 1.5rem;
max-height: 12rem;
overflow-y: auto;
outline: none;
overflow-wrap: break-word;
word-wrap: break-word;
height: 100%;
width: 100%;
display: block !important;

.pf-v6-c-form-control__textarea:focus-visible {
outline: none;
}
position: relative;
}
10 changes: 5 additions & 5 deletions packages/module/src/MessageBar/MessageBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('Message bar', () => {
render(<MessageBar onSendMessage={spy} />);
const input = screen.getByRole('textbox', { name: /Send a message.../i });
await userEvent.type(input, 'Hello world');
expect(input).toHaveValue('Hello world');
expect(input).toHaveTextContent('Hello world');
await userEvent.type(input, '[Enter]');
expect(spy).toHaveBeenCalledTimes(1);
});
Expand All @@ -92,7 +92,7 @@ describe('Message bar', () => {
render(<MessageBar onSendMessage={jest.fn} onChange={spy} />);
const input = screen.getByRole('textbox', { name: /Send a message.../i });
await userEvent.type(input, 'A');
expect(input).toHaveValue('A');
expect(input).toHaveTextContent('A');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(expect.any(Object), 'A');
});
Expand All @@ -103,14 +103,14 @@ describe('Message bar', () => {
render(<MessageBar onSendMessage={jest.fn} />);
const input = screen.getByRole('textbox', { name: /Send a message.../i });
await userEvent.type(input, 'Hello world');
expect(input).toHaveValue('Hello world');
expect(input).toHaveTextContent('Hello world');
expect(screen.getByRole('button', { name: 'Send button' })).toBeTruthy();
});
it('can disable send button shown when text is input', async () => {
render(<MessageBar onSendMessage={jest.fn} isSendButtonDisabled />);
const input = screen.getByRole('textbox', { name: /Send a message.../i });
await userEvent.type(input, 'Hello world');
expect(input).toHaveValue('Hello world');
expect(input).toHaveTextContent('Hello world');
expect(screen.getByRole('button', { name: 'Send button' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Send button' })).toBeDisabled();
});
Expand All @@ -119,7 +119,7 @@ describe('Message bar', () => {
render(<MessageBar onSendMessage={spy} />);
const input = screen.getByRole('textbox', { name: /Send a message.../i });
await userEvent.type(input, 'Hello world');
expect(input).toHaveValue('Hello world');
expect(input).toHaveTextContent('Hello world');
const sendButton = screen.getByRole('button', { name: 'Send button' });
expect(sendButton).toBeTruthy();
await userEvent.click(sendButton);
Expand Down
68 changes: 51 additions & 17 deletions packages/module/src/MessageBar/MessageBar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import { ButtonProps, DropEvent, TextAreaProps } from '@patternfly/react-core';
import { AutoTextArea } from 'react-textarea-auto-witdth-height';
import { ButtonProps, DropEvent } from '@patternfly/react-core';

// Import Chatbot components
import SendButton from './SendButton';
import MicrophoneButton from './MicrophoneButton';
import { AttachButton } from './AttachButton';
import AttachMenu from '../AttachMenu';
import StopButton from './StopButton';
import DOMPurify from 'dompurify';

export interface MessageBarWithAttachMenuProps {
/** Flag to enable whether attach menu is open */
Expand All @@ -30,7 +30,7 @@ export interface MessageBarWithAttachMenuProps {
onAttachMenuOpenChange?: (isOpen: boolean) => void;
}

export interface MessageBarProps extends TextAreaProps {
export interface MessageBarProps {
/** Callback to get the value of input message by user */
onSendMessage: (message: string) => void;
/** Class Name for the MessageBar component */
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface MessageBarProps extends TextAreaProps {
};
};
/** A callback for when the text area value changes. */
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>, value: string) => void;
onChange?: (event: React.ChangeEvent<HTMLDivElement>, value: string) => void;
}

export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
Expand All @@ -84,19 +84,40 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
// --------------------------------------------------------------------------
const [message, setMessage] = React.useState<string>('');
const [isListeningMessage, setIsListeningMessage] = React.useState<boolean>(false);

const textareaRef = React.useRef(null);
const [showPlaceholder, setShowPlaceholder] = React.useState(true);
const textareaRef = React.useRef<HTMLDivElement>(null);
const attachButtonRef = React.useRef<HTMLButtonElement>(null);

const handleChange = React.useCallback((event) => {
onChange && onChange(event, event.target.value);
setMessage(event.target.value);
}, []);
const handleInput = (event) => {
// newMessage === '' doesn't work unless we trim, which causes other problems
// textContent seems to work, but doesn't allow for markdown, so we need both
const messageText = DOMPurify.sanitize(event.target.textContent);
if (messageText === '') {
setShowPlaceholder(true);
setMessage('');
onChange && onChange(event, '');
} else {
setShowPlaceholder(false);
// this is so that tests work; RTL doesn't seem to like event.target.innerText, but browsers don't pick up markdown without it
let newMessage = messageText;
if (event.target.innerText) {
newMessage = DOMPurify.sanitize(event.target.innerText);
}
setMessage(newMessage);
onChange && onChange(event, newMessage);
}
};

// Handle sending message
const handleSend = React.useCallback(() => {
setMessage((m) => {
onSendMessage(m);
setMessage('');
if (textareaRef.current) {
textareaRef.current.innerText = '';
setShowPlaceholder(true);
textareaRef.current.blur();
}
return '';
});
}, [onSendMessage]);
Expand Down Expand Up @@ -170,19 +191,32 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
);
};

const placeholder = isListeningMessage ? 'Listening' : 'Send a message...';

const messageBarContents = (
<>
<div className="pf-chatbot__message-bar-input">
<AutoTextArea
ref={textareaRef}
{(showPlaceholder || message === '') && (
<div className="pf-chatbot__message-bar-placeholder">{placeholder}</div>
)}
<div
contentEditable
suppressContentEditableWarning={true}
role="textbox"
aria-multiline="false"
className="pf-chatbot__message-textarea"
value={message as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea
onChange={handleChange as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea
onInput={handleInput}
onFocus={() => setShowPlaceholder(false)}
onBlur={() => {
if (message === '') {
setShowPlaceholder(!showPlaceholder);
}
}}
aria-label={placeholder}
ref={textareaRef}
onKeyDown={handleKeyDown}
placeholder={isListeningMessage ? 'Listening' : 'Send a message...'}
aria-label={isListeningMessage ? 'Listening' : 'Send a message...'}
{...props}
/>
></div>
</div>
<div className="pf-chatbot__message-bar-actions">{renderButtons()}</div>
</>
Expand Down

0 comments on commit 8b55ef1

Please sign in to comment.