Chat Composer
A Chat Composer is an input made for users to type rich chat messages.
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} placeholder="Chat text" ariaLabel="A basic chat composer" />
A Chat Composer is an input made for users to type rich chat messages. Chat Composer is best used as one part of larger chat user interface to provide a seamless authoring experience.
Within the context of Paste, Chat Composer is most typically used alongside the Chat Log
component.
Chat Composer supports a variety of aria attributes which are passed into the content editable region of the component.
- If the surrounding UI includes a screen reader visible label reference the label element using
aria-labelledby
. - If the surrounding UI does not include a screen reader visible label, use
aria-label
to describe the input. - If the surrounding UI includes additional help or error text use
aria-describedby
to reference the associated element.
Chat Composer is built on top of the Lexical editor. Lexical is extensible and follows a declarative approach to configuration via JSX. Developers can leverage a
wide variety of existing plugins via the @twilio-paste/lexical-library
package or other
sources. Alternatively, developers can write their own custom plugin logic. Plugins are provided to the Chat Composer via the children
prop.
Chat Composer uses a custom AutoLinkPlugin
internally
which you can see being configured here as a JSX child.
Set a placeholder value using a placeholder
prop.
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} placeholder="Chat text" ariaLabel="A placeholder chat composer" />
Set an initial value using an initialValue
prop. This prop is limited to providing single line strings. For more complicated initial values interact with the Lexical API directly
using the config
prop and editorState
callback.
<ChatComposer config={{namespace: 'customer-chat', onError: (e) => { throw e } }} initialValue="This is my initial value" ariaLabel="An initial value chat composer" />
Restrict the height of the composer using a maxHeight
prop.
const MaxHeightExample = () => {return (<ChatComposermaxHeight="size10"ariaLabel="A max height chat composer"config={{namespace: 'customer-chat',onError (e) { throw e },editorState () {const root = $getRoot();if (root.getFirstChild() !== null) return;for (let i = 0; i < 10; i++) {root.append($createParagraphNode().append($createTextNode('this is a really really long initial value')));}},}}/>)}render(<MaxHeightExample />)
Set a rich text value using one of the Lexical formatting APIs such as toggleFormat
const RichTextExample = () => {return (<ChatComposerariaLabel="A rich text chat composer"config={{namespace: 'customer-chat',onError (e) { throw e },editorState () {const root = $getRoot();if (root.getFirstChild() !== null) return;root.append($createParagraphNode().append($createTextNode('Hello '),$createTextNode('world! ').toggleFormat('bold'),$createTextNode('This is a '),$createTextNode('chat composer ').toggleFormat('italic'),$createTextNode('with rich text functionality.')));},}}/>)}render(<RichTextExample/>)
Use Chat Composer alongside other Paste components such as Chat Log to build more complex chat UI.
const ChatDialog = () => {const {chats, push} = useChatLogger({content: (<ChatBookend><ChatBookendItem>Today</ChatBookendItem><ChatBookendItem><strong>Chat Started</strong>・3:34 PM</ChatBookendItem></ChatBookend>),},{variant: 'inbound',content: (<ChatMessage variant="inbound"><ChatBubble>Quisque ullamcorper ipsum vitae lorem euismod sodales.</ChatBubble><ChatBubble><ChatAttachment attachmentIcon={<DownloadIcon color="colorTextIcon" decorative />}><ChatAttachmentLink href="www.google.com">Document-FINAL.doc</ChatAttachmentLink><ChatAttachmentDescription>123 MB</ChatAttachmentDescription></ChatAttachment></ChatBubble><ChatMessageMeta aria-label="said by Gibby Radki at 5:04pm"><ChatMessageMetaItem>Gibby Radki ・ 5:04 PM</ChatMessageMetaItem></ChatMessageMeta></ChatMessage>),},{content: (<ChatEvent><strong>Lauren Gardner</strong> has joined the chat ・ 4:26 PM</ChatEvent>),},{variant: 'inbound',content: (<ChatMessage variant="inbound"><ChatBubble>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ChatBubble><ChatMessageMeta aria-label="said by Lauren Gardner at 4:30pm"><ChatMessageMetaItem><Avatar name="Lauren Gardner" size="sizeIcon20" />Lauren Gardner ・ 4:30 PM</ChatMessageMetaItem></ChatMessageMeta></ChatMessage>),});const [message, setMessage] = React.useState('');const [mounted, setMounted] = React.useState(false);const loggerRef = React.useRef(null);const scrollerRef = React.useRef(null);React.useEffect(() => {setMounted(true);}, []);React.useEffect(() => {if (!mounted || !loggerRef.current) return;scrollerRef.current?.scrollTo({top: loggerRef.current.scrollHeight, behavior: 'smooth'});}, [chats, mounted]);const handleComposerChange = (editorState) => {editorState.read(() => {const text = $getRoot().getTextContent();setMessage(text);});};const submitMessage = () => {if (message === '') return;push(createNewMessage(message));};return (<Box><Box ref={scrollerRef} overflowX="hidden" overflowY="auto" maxHeight="size50" tabIndex={0}><ChatLogger ref={loggerRef} chats={chats} /></Box><BoxborderStyle="solid"borderWidth="borderWidth0"borderTopWidth="borderWidth10"borderColor="colorBorderWeak"display="flex"flexDirection="row"columnGap="space30"paddingX="space70"paddingTop="space50"><ChatComposermaxHeight="size10"config={{namespace: 'foo',onError: (error) => {throw error;},}}ariaLabel="Message"placeholder="Type here..."onChange={handleComposerChange}><ClearEditorPlugin /><SendButtonPlugin onClick={submitMessage} /><EnterKeySubmitPlugin onKeyDown={submitMessage} /></ChatComposer></Box></Box>);};render(<ChatDialog />)
In the above example, we're using 3 Lexical plugins: ClearEditorPlugin
that is provided by Lexical, and 2 custom plugins, SendButtonPlugin
and EnterKeySubmitPlugin
. We also keep track of the content provided to the composer via the onChange
handler. Together we can add custom interactivity such as:
- Clear the editor on button click using
ClearEditorPlugin
- Submit on enter key press using
EnterKeySubmitPlugin
- Submit on button click using
SendButtonPlugin
Plugins are functions that must be children of the ChatComposer
component, so that they can access the Composer context.
onChange
event handler
The onChange
handler provided to the ChatComposer
takes 3 arguments, the first of which is the editorState
. This allows us to read the current content of the editor using the utilities provided by Lexical.
$getRoot
is a utility to access the composer root ElementNode
. We can then get the text content of the editor everytime it is updated, and store it in our component state for later use.
const handleComposerChange = (editorState: EditorState): void => {
editorState.read(() => {
const text = $getRoot().getTextContent();
setMessage(text);
});
};
ClearEditorPlugin
The ClearEditorPlugin
supplied by Lexical allows you to build functionality into the composer that will clear the composer content when a certain action is performed.
When passed as a child to ChatComposer
, it will automatically register a CLEAR_EDITOR_COMMAND
. You can then dispatch this command from elsewhere to clear the composer content. In the example, we created 2 plugins: SendButtonPlugin
and EnterKeySubmitPlugin
which both dispatch the CLEAR_EDITOR_COMMAND
, and clear the composer content as a result.
<ChatComposer onChange={handleComposerChange}>
<ClearEditorPlugin />
</ChatComposer>
SendButtonPlugin
and EnterKeySubmitPlugin
are custom plugins that submit a user message and clear the composer content. They first must be passed to the ChatComposer
as a child.
<ChatComposer onChange={handleComposerChange}>
<ClearEditorPlugin />
<EnterKeySubmitPlugin />
<SendButtonPlugin />
</ChatComposer>
Once "registered" as children of ChatComposer
, the plugins gain access to the composer context and can dispatch commands. They can also return JSX to be rendered into the composer. Take the SendButtonPlugin
as an example:
export const SendButtonPlugin = ({onClick}: {onClick: () => void}): JSX.Element => {
// get the editor from the composer context
const [editor] = useLexicalComposerContext();
// an event handler called from custom UI can the interact with the editor to perform certain actions
const handleSend = (): void => {
onClick();
editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
};
return (
<Box position="absolute" top="space30" right="space30">
<Button variant="primary_icon" size="reset" onClick={handleSend}>
<SendIcon decorative={false} title="Send message" />
</Button>
</Box>
);
};
Here we're rendering a button that when clicked can call a callback function, and dispatch the CLEAR_EDITOR_COMMAND
for the ClearEditorPlugin
respond to. We use it to add a new chat message in the chat log, and then clear the composer ready for the next message to be typed.