Skip to content

Commit 0a08589

Browse files
authored
Merge pull request #229 from rebeccaalpert/a11y-217
fix(MessageBox): Announce new messages
2 parents 4bdcd2c + f12aca1 commit 0a08589

File tree

6 files changed

+108
-22
lines changed

6 files changed

+108
-22
lines changed

packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Chatbot/Chatbot.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ sortValue: 2
2121
---
2222

2323
import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot';
24+
import ChatbotContent from '@patternfly/virtual-assistant/dist/dynamic/ChatbotContent';
2425
import ChatbotWelcomePrompt from '@patternfly/virtual-assistant/dist/dynamic/ChatbotWelcomePrompt';
26+
import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox';
27+
import Message from '@patternfly/virtual-assistant/dist/dynamic/Message';
2528

2629
### Container
2730

2831
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)
2932

30-
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.
33+
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.
3134

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

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

4043
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)
41-
44+
<br />
45+
<br />
4246
`<ChatbotContent>` usually contains a `<ChatbotMessageBox>` for displaying messages.
43-
47+
<br />
48+
<br />
4449
Your code structure should look like this:
4550

4651
```noLive
@@ -55,6 +60,8 @@ Your code structure should look like this:
5560
</Chatbot>
5661
```
5762

63+
**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.
64+
5865
### Welcome prompt
5966

6067
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.

packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ This demo displays a basic chatbot, which includes:
6666
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with:
6767

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

7272
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:
@@ -90,7 +90,7 @@ This demo displays an embedded chatbot. Embedded chatbots are meant to be placed
9090
3. A [`<ChatbotHeader>`](/patternfly-ai/chatbot/chatbot-header) with all built sub-components laid out, including a `<ChatbotHeaderTitle>`
9191
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with:
9292
- A `<ChatbotWelcomePrompt>`
93-
- An initial [user `<Message>`]/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [response actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
93+
- An initial [user `<Message>`](/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions)
9494
- Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef`
9595
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:
9696
- [Speech to text.](/patternfly-ai/chatbot/chatbot-footer#message-bar-with-speech-recognition-and-file-attachment)

packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx

+37-8
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@ export default MessageLoading;
9393

9494
const initialMessages: MessageProps[] = [
9595
{
96+
id: '1',
9697
role: 'user',
9798
content: 'Hello, can you give me an example of what you can do?',
9899
name: 'User',
99100
avatar: userAvatar
100101
},
101102
{
103+
id: '2',
102104
role: 'bot',
103105
content: markdown,
104106
name: 'Bot',
@@ -165,6 +167,7 @@ export const ChatbotDemo: React.FunctionComponent = () => {
165167
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
166168
initialConversations
167169
);
170+
const [announcement, setAnnouncement] = React.useState<string>();
168171
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
169172

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

195+
// you will likely want to come up with your own unique id function; this is for demo purposes only
196+
const generateId = () => {
197+
const id = Date.now() + Math.random();
198+
return id.toString();
199+
};
200+
192201
const handleSend = (message: string) => {
193202
setIsSendButtonDisabled(true);
194203
const newMessages: MessageProps[] = [];
195204
// we can't use structuredClone since messages contains functions, but we can't mutate
196205
// items that are going into state or the UI won't update correctly
197206
messages.forEach((message) => newMessages.push(message));
198-
newMessages.push({ role: 'user', content: message, name: 'User', avatar: userAvatar });
207+
newMessages.push({ id: generateId(), role: 'user', content: message, name: 'User', avatar: userAvatar });
199208
newMessages.push({
209+
id: generateId(),
200210
role: 'bot',
201211
content: 'API response goes here',
202-
name: 'bot',
212+
name: 'Bot',
203213
isLoading: true,
204214
avatar: patternflyAvatar
205215
});
206216
setMessages(newMessages);
217+
// make announcement to assistive devices that new messages have been added
218+
setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);
207219

208220
// this is for demo purposes only; in a real situation, there would be an API response we would wait for
209221
setTimeout(() => {
@@ -213,9 +225,10 @@ export const ChatbotDemo: React.FunctionComponent = () => {
213225
newMessages.forEach((message) => loadedMessages.push(message));
214226
loadedMessages.pop();
215227
loadedMessages.push({
228+
id: generateId(),
216229
role: 'bot',
217230
content: 'API response goes here',
218-
name: 'bot',
231+
name: 'Bot',
219232
isLoading: false,
220233
avatar: patternflyAvatar,
221234
actions: {
@@ -232,6 +245,8 @@ export const ChatbotDemo: React.FunctionComponent = () => {
232245
}
233246
});
234247
setMessages(loadedMessages);
248+
// make announcement to assistive devices that new message has loaded
249+
setAnnouncement(`Message from Bot: API response goes here`);
235250
setIsSendButtonDisabled(false);
236251
}, 5000);
237252
};
@@ -357,16 +372,30 @@ export const ChatbotDemo: React.FunctionComponent = () => {
357372
</ChatbotHeaderActions>
358373
</ChatbotHeader>
359374
<ChatbotContent>
360-
<MessageBox>
375+
{/* Update the announcement prop on MessageBox whenever a new message is sent
376+
so that users of assistive devices receive sufficient context */}
377+
<MessageBox announcement={announcement}>
361378
<ChatbotWelcomePrompt
362379
title="Hello, Chatbot User"
363380
description="How may I help you today?"
364381
prompts={welcomePrompts}
365382
/>
366-
{messages.map((message) => (
367-
<Message key={message.name} {...message} />
368-
))}
369-
<div ref={scrollToBottomRef}></div>
383+
{/* This code block enables scrolling to the top of the last message.
384+
You can instead choose to move the div with scrollToBottomRef on it below
385+
the map of messages, so that users are forced to scroll to the bottom.
386+
If you are using streaming, you will want to take a different approach;
387+
see: https://github.com/patternfly/virtual-assistant/issues/201#issuecomment-2400725173 */}
388+
{messages.map((message, index) => {
389+
if (index === messages.length - 1) {
390+
return (
391+
<>
392+
<div ref={scrollToBottomRef}></div>
393+
<Message key={message.id} {...message} />
394+
</>
395+
);
396+
}
397+
return <Message key={message.id} {...message} />;
398+
})}
370399
</MessageBox>
371400
</ChatbotContent>
372401
<ChatbotFooter>

packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/EmbeddedChatbot.tsx

+37-8
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,14 @@ export default MessageLoading;
100100

101101
const initialMessages: MessageProps[] = [
102102
{
103+
id: '1',
103104
role: 'user',
104105
content: 'Hello, can you give me an example of what you can do?',
105106
name: 'User',
106107
avatar: userAvatar
107108
},
108109
{
110+
id: '2',
109111
role: 'bot',
110112
content: markdown,
111113
name: 'Bot',
@@ -171,6 +173,7 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
171173
initialConversations
172174
);
173175
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
176+
const [announcement, setAnnouncement] = React.useState<string>();
174177
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
175178
const displayMode = ChatbotDisplayMode.embedded;
176179
// Autu-scrolls to the latest message
@@ -188,21 +191,30 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
188191
setSelectedModel(value as string);
189192
};
190193

194+
// you will likely want to come up with your own unique id function; this is for demo purposes only
195+
const generateId = () => {
196+
const id = Date.now() + Math.random();
197+
return id.toString();
198+
};
199+
191200
const handleSend = (message: string) => {
192201
setIsSendButtonDisabled(true);
193202
const newMessages: MessageProps[] = [];
194203
// we can't use structuredClone since messages contains functions, but we can't mutate
195204
// items that are going into state or the UI won't update correctly
196205
messages.forEach((message) => newMessages.push(message));
197-
newMessages.push({ role: 'user', content: message, name: 'User', avatar: userAvatar });
206+
newMessages.push({ id: generateId(), role: 'user', content: message, name: 'User', avatar: userAvatar });
198207
newMessages.push({
208+
id: generateId(),
199209
role: 'bot',
200210
content: 'API response goes here',
201-
name: 'bot',
211+
name: 'Bot',
202212
avatar: patternflyAvatar,
203213
isLoading: true
204214
});
205215
setMessages(newMessages);
216+
// make announcement to assistive devices that new messages have been added
217+
setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`);
206218

207219
// this is for demo purposes only; in a real situation, there would be an API response we would wait for
208220
setTimeout(() => {
@@ -212,9 +224,10 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
212224
newMessages.forEach((message) => loadedMessages.push(message));
213225
loadedMessages.pop();
214226
loadedMessages.push({
227+
id: generateId(),
215228
role: 'bot',
216229
content: 'API response goes here',
217-
name: 'bot',
230+
name: 'Bot',
218231
avatar: patternflyAvatar,
219232
isLoading: false,
220233
actions: {
@@ -231,6 +244,8 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
231244
}
232245
});
233246
setMessages(loadedMessages);
247+
// make announcement to assistive devices that new message has loaded
248+
setAnnouncement(`Message from Bot: API response goes here`);
234249
setIsSendButtonDisabled(false);
235250
}, 5000);
236251
};
@@ -339,16 +354,30 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
339354
</ChatbotHeaderActions>
340355
</ChatbotHeader>
341356
<ChatbotContent>
342-
<MessageBox>
357+
{/* Update the announcement prop on MessageBox whenever a new message is sent
358+
so that users of assistive devices receive sufficient context */}
359+
<MessageBox announcement={announcement}>
343360
<ChatbotWelcomePrompt
344361
title="Hello, Chatbot User"
345362
description="How may I help you today?"
346363
prompts={welcomePrompts}
347364
/>
348-
{messages.map((message) => (
349-
<Message key={message.name} {...message} />
350-
))}
351-
<div ref={scrollToBottomRef}></div>
365+
{/* This code block enables scrolling to the top of the last message.
366+
You can instead choose to move the div with scrollToBottomRef on it below
367+
the map of messages, so that users are forced to scroll to the bottom.
368+
If you are using streaming, you will want to take a different approach;
369+
see: https://github.com/patternfly/virtual-assistant/issues/201#issuecomment-2400725173 */}
370+
{messages.map((message, index) => {
371+
if (index === messages.length - 1) {
372+
return (
373+
<>
374+
<div ref={scrollToBottomRef}></div>
375+
<Message key={message.id} {...message} />
376+
</>
377+
);
378+
}
379+
return <Message key={message.id} {...message} />;
380+
})}
352381
</MessageBox>
353382
</ChatbotContent>
354383
<ChatbotFooter>

packages/module/src/MessageBox/MessageBox.scss

+12
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@
88
row-gap: var(--pf-t--global--spacer--sm);
99
padding: 0 var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg);
1010
}
11+
12+
// hide from view but not assistive technologies
13+
// https://css-tricks.com/inclusively-hidden/
14+
.pf-chatbot__messagebox-announcement {
15+
clip: rect(0 0 0 0);
16+
clip-path: inset(50%);
17+
height: 1px;
18+
overflow: hidden;
19+
position: absolute;
20+
white-space: nowrap;
21+
width: 1px;
22+
}

packages/module/src/MessageBox/MessageBox.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ export interface MessageBoxProps extends React.HTMLProps<HTMLDivElement> {
99
children: React.ReactNode;
1010
/** Custom classname for the MessageBox component */
1111
className?: string;
12+
/** Content that can be announced, such as a new message, for screen readers */
13+
announcement?: string;
1214
}
1315

14-
const MessageBox: React.FunctionComponent<MessageBoxProps> = ({ children, className }: MessageBoxProps) => {
16+
const MessageBox: React.FunctionComponent<MessageBoxProps> = ({
17+
announcement,
18+
children,
19+
className
20+
}: MessageBoxProps) => {
1521
const [atTop, setAtTop] = React.useState(false);
1622
const [atBottom, setAtBottom] = React.useState(true);
1723
const [isOverflowing, setIsOverflowing] = React.useState(false);
@@ -72,6 +78,9 @@ const MessageBox: React.FunctionComponent<MessageBoxProps> = ({ children, classN
7278
<JumpButton position="top" isHidden={isOverflowing && atTop} onClick={scrollToTop} />
7379
<div className={`pf-chatbot__messagebox ${className ?? ''}`} ref={messageBoxRef}>
7480
{children}
81+
<div className="pf-chatbot__messagebox-announcement" aria-live="polite">
82+
{announcement}
83+
</div>
7584
</div>
7685
<JumpButton position="bottom" isHidden={isOverflowing && atBottom} onClick={scrollToBottom} />
7786
</>

0 commit comments

Comments
 (0)