Skip to content

Commit

Permalink
Merge pull request #229 from rebeccaalpert/a11y-217
Browse files Browse the repository at this point in the history
fix(MessageBox): Announce new messages
  • Loading branch information
nicolethoen authored Oct 14, 2024
2 parents 4bdcd2c + f12aca1 commit 0a08589
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ sortValue: 2
---

import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot';
import ChatbotContent from '@patternfly/virtual-assistant/dist/dynamic/ChatbotContent';
import ChatbotWelcomePrompt from '@patternfly/virtual-assistant/dist/dynamic/ChatbotWelcomePrompt';
import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox';
import Message from '@patternfly/virtual-assistant/dist/dynamic/Message';

### Container

The PatternFly chatbot is a separate window that overlays or is embedded within other UI content. This container can be shown and hidden via [the chatbot toggle.](/patternfly-ai/chatbot/chatbot-toggle)

The `<Chatbot>` component is the container that encompasses the chatbot experience. It adapts to various display modes (overlay/default, docked, fullscreen, and embedded) and supports both light and dark themes.
The `<Chatbot>` component is the container that encompasses the chatbot experience. It adapts to various display modes (overlay/default, docked, fullscreen, and embedded) and supports both light and dark themes.

The "embedded" display mode is meant to be used within a [PatternFly page](/components/page) or other container within your product.

Expand All @@ -38,9 +41,11 @@ The "embedded" display mode is meant to be used within a [PatternFly page](/comp
### Content and message box

The `<ChatbotContent>` component is the container that is placed within the `<Chatbot>`, between the [`<ChatbotHeader>`](/patternfly-ai/chatbot/chatbot-header) and [`<ChatbotFooter>`.](/patternfly-ai/chatbot/chatbot-footer)

<br />
<br />
`<ChatbotContent>` usually contains a `<ChatbotMessageBox>` for displaying messages.

<br />
<br />
Your code structure should look like this:

```noLive
Expand All @@ -55,6 +60,8 @@ Your code structure should look like this:
</Chatbot>
```

**Note**: When messages update, it is important to announce new messages to users of assistive technology. To do this, make sure to set the `announcement` prop on `<MessageBox>` whenever you display a new message in `<MessageBox>`. You can view this in action in our [basic chatbot](/patternfly-ai/chatbot/chatbot-container/react-demos#basic-chatbot) and [embedded chatbot](/patternfly-ai/chatbot/chatbot-container/react-demos#embedded-chatbot) demos.

### Welcome prompt

To introduce users to the chatbot experience, a welcome prompt can fill the message box before they input their first message. This brief message should follow our [conversation design guidelines](/patternfly-ai/conversation-design) to welcome users to the chatbot experience and encourage them to interact.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ This demo displays a basic chatbot, which includes:
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with:

- A `<ChatbotWelcomePrompt>`
- An initial [user `<Message>`](/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [response actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
- An initial [user `<Message>`](/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
- Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef`

5. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/chatbot-footer) with a [`<ChatbotFootNote>`](/patternfly-ai/chatbot/chatbot-footer#footnote-with-popover) and a `<MessageBar>` that contains the abilities of:
Expand All @@ -90,7 +90,7 @@ This demo displays an embedded chatbot. Embedded chatbots are meant to be placed
3. A [`<ChatbotHeader>`](/patternfly-ai/chatbot/chatbot-header) with all built sub-components laid out, including a `<ChatbotHeaderTitle>`
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with:
- A `<ChatbotWelcomePrompt>`
- An initial [user `<Message>`]/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [response actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
- An initial [user `<Message>`](/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
- Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef`
5. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/chatbot-footer) with a [`<ChatbotFootNote>`](/patternfly-ai/chatbot/chatbot-footer#footnote-with-popover) and a `<MessageBar>` that contains the abilities of:
- [Speech to text.](/patternfly-ai/chatbot/chatbot-footer#message-bar-with-speech-recognition-and-file-attachment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ export default MessageLoading;

const initialMessages: MessageProps[] = [
{
id: '1',
role: 'user',
content: 'Hello, can you give me an example of what you can do?',
name: 'User',
avatar: userAvatar
},
{
id: '2',
role: 'bot',
content: markdown,
name: 'Bot',
Expand Down Expand Up @@ -165,6 +167,7 @@ export const ChatbotDemo: React.FunctionComponent = () => {
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
initialConversations
);
const [announcement, setAnnouncement] = React.useState<string>();
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);

// Autu-scrolls to the latest message
Expand All @@ -189,21 +192,30 @@ export const ChatbotDemo: React.FunctionComponent = () => {
setDisplayMode(value as ChatbotDisplayMode);
};

// you will likely want to come up with your own unique id function; this is for demo purposes only
const generateId = () => {
const id = Date.now() + Math.random();
return id.toString();
};

const handleSend = (message: string) => {
setIsSendButtonDisabled(true);
const newMessages: MessageProps[] = [];
// we can't use structuredClone since messages contains functions, but we can't mutate
// items that are going into state or the UI won't update correctly
messages.forEach((message) => newMessages.push(message));
newMessages.push({ role: 'user', content: message, name: 'User', avatar: userAvatar });
newMessages.push({ id: generateId(), role: 'user', content: message, name: 'User', avatar: userAvatar });
newMessages.push({
id: generateId(),
role: 'bot',
content: 'API response goes here',
name: 'bot',
name: 'Bot',
isLoading: true,
avatar: patternflyAvatar
});
setMessages(newMessages);
// make announcement to assistive devices that new messages have been added
setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);

// this is for demo purposes only; in a real situation, there would be an API response we would wait for
setTimeout(() => {
Expand All @@ -213,9 +225,10 @@ export const ChatbotDemo: React.FunctionComponent = () => {
newMessages.forEach((message) => loadedMessages.push(message));
loadedMessages.pop();
loadedMessages.push({
id: generateId(),
role: 'bot',
content: 'API response goes here',
name: 'bot',
name: 'Bot',
isLoading: false,
avatar: patternflyAvatar,
actions: {
Expand All @@ -232,6 +245,8 @@ export const ChatbotDemo: React.FunctionComponent = () => {
}
});
setMessages(loadedMessages);
// make announcement to assistive devices that new message has loaded
setAnnouncement(`Message from Bot: API response goes here`);
setIsSendButtonDisabled(false);
}, 5000);
};
Expand Down Expand Up @@ -357,16 +372,30 @@ export const ChatbotDemo: React.FunctionComponent = () => {
</ChatbotHeaderActions>
</ChatbotHeader>
<ChatbotContent>
<MessageBox>
{/* Update the announcement prop on MessageBox whenever a new message is sent
so that users of assistive devices receive sufficient context */}
<MessageBox announcement={announcement}>
<ChatbotWelcomePrompt
title="Hello, Chatbot User"
description="How may I help you today?"
prompts={welcomePrompts}
/>
{messages.map((message) => (
<Message key={message.name} {...message} />
))}
<div ref={scrollToBottomRef}></div>
{/* This code block enables scrolling to the top of the last message.
You can instead choose to move the div with scrollToBottomRef on it below
the map of messages, so that users are forced to scroll to the bottom.
If you are using streaming, you will want to take a different approach;
see: https://github.com/patternfly/virtual-assistant/issues/201#issuecomment-2400725173 */}
{messages.map((message, index) => {
if (index === messages.length - 1) {
return (
<>
<div ref={scrollToBottomRef}></div>
<Message key={message.id} {...message} />
</>
);
}
return <Message key={message.id} {...message} />;
})}
</MessageBox>
</ChatbotContent>
<ChatbotFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ export default MessageLoading;

const initialMessages: MessageProps[] = [
{
id: '1',
role: 'user',
content: 'Hello, can you give me an example of what you can do?',
name: 'User',
avatar: userAvatar
},
{
id: '2',
role: 'bot',
content: markdown,
name: 'Bot',
Expand Down Expand Up @@ -171,6 +173,7 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
initialConversations
);
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const [announcement, setAnnouncement] = React.useState<string>();
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
const displayMode = ChatbotDisplayMode.embedded;
// Autu-scrolls to the latest message
Expand All @@ -188,21 +191,30 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
setSelectedModel(value as string);
};

// you will likely want to come up with your own unique id function; this is for demo purposes only
const generateId = () => {
const id = Date.now() + Math.random();
return id.toString();
};

const handleSend = (message: string) => {
setIsSendButtonDisabled(true);
const newMessages: MessageProps[] = [];
// we can't use structuredClone since messages contains functions, but we can't mutate
// items that are going into state or the UI won't update correctly
messages.forEach((message) => newMessages.push(message));
newMessages.push({ role: 'user', content: message, name: 'User', avatar: userAvatar });
newMessages.push({ id: generateId(), role: 'user', content: message, name: 'User', avatar: userAvatar });
newMessages.push({
id: generateId(),
role: 'bot',
content: 'API response goes here',
name: 'bot',
name: 'Bot',
avatar: patternflyAvatar,
isLoading: true
});
setMessages(newMessages);
// make announcement to assistive devices that new messages have been added
setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);

// this is for demo purposes only; in a real situation, there would be an API response we would wait for
setTimeout(() => {
Expand All @@ -212,9 +224,10 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
newMessages.forEach((message) => loadedMessages.push(message));
loadedMessages.pop();
loadedMessages.push({
id: generateId(),
role: 'bot',
content: 'API response goes here',
name: 'bot',
name: 'Bot',
avatar: patternflyAvatar,
isLoading: false,
actions: {
Expand All @@ -231,6 +244,8 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
}
});
setMessages(loadedMessages);
// make announcement to assistive devices that new message has loaded
setAnnouncement(`Message from Bot: API response goes here`);
setIsSendButtonDisabled(false);
}, 5000);
};
Expand Down Expand Up @@ -339,16 +354,30 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
</ChatbotHeaderActions>
</ChatbotHeader>
<ChatbotContent>
<MessageBox>
{/* Update the announcement prop on MessageBox whenever a new message is sent
so that users of assistive devices receive sufficient context */}
<MessageBox announcement={announcement}>
<ChatbotWelcomePrompt
title="Hello, Chatbot User"
description="How may I help you today?"
prompts={welcomePrompts}
/>
{messages.map((message) => (
<Message key={message.name} {...message} />
))}
<div ref={scrollToBottomRef}></div>
{/* This code block enables scrolling to the top of the last message.
You can instead choose to move the div with scrollToBottomRef on it below
the map of messages, so that users are forced to scroll to the bottom.
If you are using streaming, you will want to take a different approach;
see: https://github.com/patternfly/virtual-assistant/issues/201#issuecomment-2400725173 */}
{messages.map((message, index) => {
if (index === messages.length - 1) {
return (
<>
<div ref={scrollToBottomRef}></div>
<Message key={message.id} {...message} />
</>
);
}
return <Message key={message.id} {...message} />;
})}
</MessageBox>
</ChatbotContent>
<ChatbotFooter>
Expand Down
12 changes: 12 additions & 0 deletions packages/module/src/MessageBox/MessageBox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,15 @@
row-gap: var(--pf-t--global--spacer--sm);
padding: 0 var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg);
}

// hide from view but not assistive technologies
// https://css-tricks.com/inclusively-hidden/
.pf-chatbot__messagebox-announcement {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
11 changes: 10 additions & 1 deletion packages/module/src/MessageBox/MessageBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ export interface MessageBoxProps extends React.HTMLProps<HTMLDivElement> {
children: React.ReactNode;
/** Custom classname for the MessageBox component */
className?: string;
/** Content that can be announced, such as a new message, for screen readers */
announcement?: string;
}

const MessageBox: React.FunctionComponent<MessageBoxProps> = ({ children, className }: MessageBoxProps) => {
const MessageBox: React.FunctionComponent<MessageBoxProps> = ({
announcement,
children,
className
}: MessageBoxProps) => {
const [atTop, setAtTop] = React.useState(false);
const [atBottom, setAtBottom] = React.useState(true);
const [isOverflowing, setIsOverflowing] = React.useState(false);
Expand Down Expand Up @@ -72,6 +78,9 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = ({ children, classN
<JumpButton position="top" isHidden={isOverflowing && atTop} onClick={scrollToTop} />
<div className={`pf-chatbot__messagebox ${className ?? ''}`} ref={messageBoxRef}>
{children}
<div className="pf-chatbot__messagebox-announcement" aria-live="polite">
{announcement}
</div>
</div>
<JumpButton position="bottom" isHidden={isOverflowing && atBottom} onClick={scrollToBottom} />
</>
Expand Down

0 comments on commit 0a08589

Please sign in to comment.