From f12aca1018483dc94def59ee994ca69e3b48c1c8 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 11 Oct 2024 19:59:31 -0400 Subject: [PATCH] fix(MessageBox): Announce new messages Messages should be announced to assistive devices. I added a prop to MessageBox to allow for this and updated our two demos with message sending so they make announcements. I also documented this in the Chatbot container docs. I checked this worked with VoiceOver, but I can't speak to any other tools. --- .../examples/Chatbot/Chatbot.md | 13 ++++-- .../examples/demos/Chatbot.md | 4 +- .../examples/demos/Chatbot.tsx | 45 +++++++++++++++---- .../examples/demos/EmbeddedChatbot.tsx | 45 +++++++++++++++---- .../module/src/MessageBox/MessageBox.scss | 12 +++++ packages/module/src/MessageBox/MessageBox.tsx | 11 ++++- 6 files changed, 108 insertions(+), 22 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Chatbot/Chatbot.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Chatbot/Chatbot.md index c6ad87cd..10a05a0d 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Chatbot/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Chatbot/Chatbot.md @@ -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 `` 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 `` 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. @@ -38,9 +41,11 @@ The "embedded" display mode is meant to be used within a [PatternFly page](/comp ### Content and message box The `` component is the container that is placed within the ``, between the [``](/patternfly-ai/chatbot/chatbot-header) and [``.](/patternfly-ai/chatbot/chatbot-footer) - +
+
`` usually contains a `` for displaying messages. - +
+
Your code structure should look like this: ```noLive @@ -55,6 +60,8 @@ Your code structure should look like this:
``` +**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 `` whenever you display a new message in ``. 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. diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md index c4c6f297..49959439 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md @@ -66,7 +66,7 @@ This demo displays a basic chatbot, which includes: 4. [`` and ``](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with: - A `` -- An initial [user ``](/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [response actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions) +- An initial [user ``](/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 [``](/patternfly-ai/chatbot/chatbot-footer) with a [``](/patternfly-ai/chatbot/chatbot-footer#footnote-with-popover) and a `` that contains the abilities of: @@ -90,7 +90,7 @@ This demo displays an embedded chatbot. Embedded chatbots are meant to be placed 3. A [``](/patternfly-ai/chatbot/chatbot-header) with all built sub-components laid out, including a `` 4. [`` and ``](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with: - A `` - - An initial [user ``]/patternfly-ai/chatbot/chatbot-messages) and an initial bot message with [response actions.](/patternfly-ai/chatbot/chatbot-messages/#messages-actions) + - An initial [user ``](/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 [``](/patternfly-ai/chatbot/chatbot-footer) with a [``](/patternfly-ai/chatbot/chatbot-footer#footnote-with-popover) and a `` that contains the abilities of: - [Speech to text.](/patternfly-ai/chatbot/chatbot-footer#message-bar-with-speech-recognition-and-file-attachment) diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx index 942e301e..f65064fc 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx @@ -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', @@ -165,6 +167,7 @@ export const ChatbotDemo: React.FunctionComponent = () => { const [conversations, setConversations] = React.useState( initialConversations ); + const [announcement, setAnnouncement] = React.useState(); const scrollToBottomRef = React.useRef(null); // Autu-scrolls to the latest message @@ -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(() => { @@ -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: { @@ -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); }; @@ -357,16 +372,30 @@ export const ChatbotDemo: React.FunctionComponent = () => { - + {/* Update the announcement prop on MessageBox whenever a new message is sent + so that users of assistive devices receive sufficient context */} + - {messages.map((message) => ( - - ))} -
+ {/* 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 ( + <> +
+ + + ); + } + return ; + })}
diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/EmbeddedChatbot.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/EmbeddedChatbot.tsx index b3500f3b..7e53eaef 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/EmbeddedChatbot.tsx +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/EmbeddedChatbot.tsx @@ -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', @@ -171,6 +173,7 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => { initialConversations ); const [isSidebarOpen, setIsSidebarOpen] = React.useState(false); + const [announcement, setAnnouncement] = React.useState(); const scrollToBottomRef = React.useRef(null); const displayMode = ChatbotDisplayMode.embedded; // Autu-scrolls to the latest message @@ -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(() => { @@ -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: { @@ -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); }; @@ -339,16 +354,30 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => { - + {/* Update the announcement prop on MessageBox whenever a new message is sent + so that users of assistive devices receive sufficient context */} + - {messages.map((message) => ( - - ))} -
+ {/* 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 ( + <> +
+ + + ); + } + return ; + })}
diff --git a/packages/module/src/MessageBox/MessageBox.scss b/packages/module/src/MessageBox/MessageBox.scss index 3ca2895e..8641479a 100644 --- a/packages/module/src/MessageBox/MessageBox.scss +++ b/packages/module/src/MessageBox/MessageBox.scss @@ -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; +} diff --git a/packages/module/src/MessageBox/MessageBox.tsx b/packages/module/src/MessageBox/MessageBox.tsx index 497afffb..62a87176 100644 --- a/packages/module/src/MessageBox/MessageBox.tsx +++ b/packages/module/src/MessageBox/MessageBox.tsx @@ -9,9 +9,15 @@ export interface MessageBoxProps extends React.HTMLProps { 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 = ({ children, className }: MessageBoxProps) => { +const MessageBox: React.FunctionComponent = ({ + announcement, + children, + className +}: MessageBoxProps) => { const [atTop, setAtTop] = React.useState(false); const [atBottom, setAtBottom] = React.useState(true); const [isOverflowing, setIsOverflowing] = React.useState(false); @@ -72,6 +78,9 @@ const MessageBox: React.FunctionComponent = ({ children, classN
{children} +
+ {announcement} +