From 4396bdd162cb4af6f6419afb07df69354df22364 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 4 Nov 2024 09:29:56 -0500 Subject: [PATCH 1/2] fix(MessageBar): Swap out textarea --- package-lock.json | 7 +++ package.json | 1 + .../module/src/MessageBar/MessageBar.scss | 22 ++++----- .../module/src/MessageBar/MessageBar.test.tsx | 10 ++-- packages/module/src/MessageBar/MessageBar.tsx | 48 ++++++++++++------- 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 985d445c..facdfe56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/*" ], "dependencies": { + "dompurify": "^3.2.0", "react-dropzone": "^14.2.3" }, "devDependencies": { @@ -10218,6 +10219,12 @@ "node": ">=12" } }, + "node_modules/dompurify": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz", + "integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", diff --git a/package.json b/package.json index 186701b1..7f0ae71b 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "whatwg-fetch": "^3.6.20" }, "dependencies": { + "dompurify": "^3.2.0", "react-dropzone": "^14.2.3" }, "packageManager": "yarn@4.5.0+sha512.837566d24eec14ec0f5f1411adb544e892b3454255e61fdef8fd05f3429480102806bac7446bc9daff3896b01ae4b62d00096c7e989f1596f2af10b927532f39" diff --git a/packages/module/src/MessageBar/MessageBar.scss b/packages/module/src/MessageBar/MessageBar.scss index 87c80fa8..2ac365a7 100644 --- a/packages/module/src/MessageBar/MessageBar.scss +++ b/packages/module/src/MessageBar/MessageBar.scss @@ -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); } @@ -34,8 +36,8 @@ &-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); } @@ -43,25 +45,23 @@ 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; } } -// - 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; - } } diff --git a/packages/module/src/MessageBar/MessageBar.test.tsx b/packages/module/src/MessageBar/MessageBar.test.tsx index 1e32e67c..ee44bf09 100644 --- a/packages/module/src/MessageBar/MessageBar.test.tsx +++ b/packages/module/src/MessageBar/MessageBar.test.tsx @@ -83,7 +83,7 @@ describe('Message bar', () => { render(); 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); }); @@ -92,7 +92,7 @@ describe('Message bar', () => { render(); 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'); }); @@ -103,14 +103,14 @@ describe('Message bar', () => { render(); 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(); 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(); }); @@ -119,7 +119,7 @@ describe('Message bar', () => { render(); 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); diff --git a/packages/module/src/MessageBar/MessageBar.tsx b/packages/module/src/MessageBar/MessageBar.tsx index e772fab7..251556ba 100644 --- a/packages/module/src/MessageBar/MessageBar.tsx +++ b/packages/module/src/MessageBar/MessageBar.tsx @@ -1,6 +1,5 @@ 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'; @@ -8,6 +7,7 @@ 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 */ @@ -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 */ @@ -62,7 +62,7 @@ export interface MessageBarProps extends TextAreaProps { }; }; /** A callback for when the text area value changes. */ - onChange?: (event: React.ChangeEvent, value: string) => void; + onChange?: (event: React.ChangeEvent, value: string) => void; } export const MessageBar: React.FunctionComponent = ({ @@ -84,19 +84,25 @@ export const MessageBar: React.FunctionComponent = ({ // -------------------------------------------------------------------------- const [message, setMessage] = React.useState(''); const [isListeningMessage, setIsListeningMessage] = React.useState(false); - - const textareaRef = React.useRef(null); + const [showPlaceholder, setShowPlaceholder] = React.useState(true); + const textareaRef = React.useRef(null); const attachButtonRef = React.useRef(null); - const handleChange = React.useCallback((event) => { - onChange && onChange(event, event.target.value); - setMessage(event.target.value); - }, []); + const handleInput = (event) => { + const newMessage = DOMPurify.sanitize(event.target.textContent); + setMessage(newMessage); + onChange && onChange(event, newMessage); + }; // Handle sending message const handleSend = React.useCallback(() => { setMessage((m) => { onSendMessage(m); + setMessage(''); + if (textareaRef.current) { + textareaRef.current.innerText = ''; + textareaRef.current.blur(); + } return ''; }); }, [onSendMessage]); @@ -170,19 +176,27 @@ export const MessageBar: React.FunctionComponent = ({ ); }; + const placeholder = isListeningMessage ? 'Listening' : 'Send a message...'; + const messageBarContents = ( <>
- setShowPlaceholder(!showPlaceholder)} + onBlur={() => setShowPlaceholder(!showPlaceholder)} + aria-label={placeholder} + ref={textareaRef} onKeyDown={handleKeyDown} - placeholder={isListeningMessage ? 'Listening' : 'Send a message...'} - aria-label={isListeningMessage ? 'Listening' : 'Send a message...'} {...props} - /> + > + {showPlaceholder ? placeholder : undefined} +
{renderButtons()}
From 5b752853f6074abc44c5952e69145a3c227464f2 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 18 Nov 2024 18:17:05 -0500 Subject: [PATCH 2/2] fix(MessageBar): Accept markdown input --- .../module/src/MessageBar/MessageBar.scss | 10 ++++++ packages/module/src/MessageBar/MessageBar.tsx | 36 ++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/module/src/MessageBar/MessageBar.scss b/packages/module/src/MessageBar/MessageBar.scss index 2ac365a7..807050ff 100644 --- a/packages/module/src/MessageBar/MessageBar.scss +++ b/packages/module/src/MessageBar/MessageBar.scss @@ -46,6 +46,15 @@ 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; } } @@ -64,4 +73,5 @@ height: 100%; width: 100%; display: block !important; + position: relative; } diff --git a/packages/module/src/MessageBar/MessageBar.tsx b/packages/module/src/MessageBar/MessageBar.tsx index 251556ba..6d64b208 100644 --- a/packages/module/src/MessageBar/MessageBar.tsx +++ b/packages/module/src/MessageBar/MessageBar.tsx @@ -89,9 +89,23 @@ export const MessageBar: React.FunctionComponent = ({ const attachButtonRef = React.useRef(null); const handleInput = (event) => { - const newMessage = DOMPurify.sanitize(event.target.textContent); - setMessage(newMessage); - onChange && onChange(event, newMessage); + // 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 @@ -101,6 +115,7 @@ export const MessageBar: React.FunctionComponent = ({ setMessage(''); if (textareaRef.current) { textareaRef.current.innerText = ''; + setShowPlaceholder(true); textareaRef.current.blur(); } return ''; @@ -181,6 +196,9 @@ export const MessageBar: React.FunctionComponent = ({ const messageBarContents = ( <>
+ {(showPlaceholder || message === '') && ( +
{placeholder}
+ )}
= ({ aria-multiline="false" className="pf-chatbot__message-textarea" onInput={handleInput} - onFocus={() => setShowPlaceholder(!showPlaceholder)} - onBlur={() => setShowPlaceholder(!showPlaceholder)} + onFocus={() => setShowPlaceholder(false)} + onBlur={() => { + if (message === '') { + setShowPlaceholder(!showPlaceholder); + } + }} aria-label={placeholder} ref={textareaRef} onKeyDown={handleKeyDown} {...props} - > - {showPlaceholder ? placeholder : undefined} -
+ >
{renderButtons()}