From 22cfcd3113a958b5ef789b1718ee0902423b1aa3 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 13 Jan 2025 10:38:23 -0500 Subject: [PATCH] feat(UserFeedback): Add user feedback cards --- .../examples/Messages/MessageWithFeedback.tsx | 176 +++++++++ .../Messages/MessageWithFeedbackTimeout.tsx | 52 +++ .../chatbot/examples/Messages/Messages.md | 35 +- packages/module/src/Message/Message.tsx | 25 +- .../Message/QuickResponse/QuickResponse.tsx | 8 +- .../src/Message/UserFeedback/CloseButton.tsx | 21 ++ .../Message/UserFeedback/UserFeedback.scss | 53 +++ .../UserFeedback/UserFeedback.test.tsx | 351 ++++++++++++++++++ .../src/Message/UserFeedback/UserFeedback.tsx | 225 +++++++++++ .../UserFeedbackComplete.test.tsx | 252 +++++++++++++ .../UserFeedback/UserFeedbackComplete.tsx | 204 ++++++++++ .../ResponseActions/ResponseActionButton.tsx | 14 +- .../src/ResponseActions/ResponseActions.tsx | 12 +- packages/module/src/main.scss | 1 + 14 files changed, 1419 insertions(+), 10 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedbackTimeout.tsx create mode 100644 packages/module/src/Message/UserFeedback/CloseButton.tsx create mode 100644 packages/module/src/Message/UserFeedback/UserFeedback.scss create mode 100644 packages/module/src/Message/UserFeedback/UserFeedback.test.tsx create mode 100644 packages/module/src/Message/UserFeedback/UserFeedback.tsx create mode 100644 packages/module/src/Message/UserFeedback/UserFeedbackComplete.test.tsx create mode 100644 packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx new file mode 100644 index 00000000..c913c184 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithFeedbackExample: React.FunctionComponent = () => { + const [showUserFeedbackForm, setShowUserFeedbackForm] = React.useState(false); + const [showCompletionForm, setShowCompletionForm] = React.useState(false); + const [launchButton, setLaunchButton] = React.useState(); + const positiveRef = React.useRef(null); + const negativeRef = React.useRef(null); + const feedbackId = 'user-feedback-form'; + const completeId = 'user-feedback-received'; + + const getCurrentCard = () => { + if (showUserFeedbackForm) { + return feedbackId; + } + if (showCompletionForm) { + return completeId; + } + }; + + const isExpanded = showUserFeedbackForm || showCompletionForm; + + const focusLaunchButton = () => { + if (launchButton === 'positive') { + positiveRef.current?.focus(); + } + if (launchButton === 'negative') { + negativeRef.current?.focus(); + } + }; + + return ( + <> + { + setShowUserFeedbackForm(true); + setShowCompletionForm(false); + setLaunchButton('positive'); + }, + /* These are important for accessibility */ + 'aria-expanded': isExpanded, + 'aria-controls': getCurrentCard(), + isClicked: launchButton === 'positive', + ref: positiveRef + }, + negative: { + onClick: () => { + setShowUserFeedbackForm(true); + setShowCompletionForm(false); + setLaunchButton('negative'); + }, + /* These are important for accessibility */ + 'aria-expanded': isExpanded, + 'aria-controls': getCurrentCard(), + isClicked: launchButton === 'negative', + ref: negativeRef + } + }} + userFeedbackForm={ + showUserFeedbackForm + ? /* eslint-disable indent */ + { + quickResponses: [ + { id: '1', content: 'Correct' }, + { id: '2', content: 'Easy to understand' }, + { id: '3', content: 'Complete' } + ], + onSubmit: (quickResponse, additionalFeedback) => { + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`); + setShowUserFeedbackForm(false); + setShowCompletionForm(true); + focusLaunchButton(); + }, + hasTextArea: true, + onClose: () => { + setShowUserFeedbackForm(false); + focusLaunchButton(); + }, + id: feedbackId + } + : undefined + /* eslint-enable indent */ + } + userFeedbackComplete={ + showCompletionForm + ? /* eslint-disable indent */ + { + onClose: () => { + setShowCompletionForm(false); + focusLaunchButton(); + }, + id: completeId + } + : undefined + /* eslint-enable indent */ + } + /> + + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), + hasTextArea: true, + // eslint-disable-next-line no-console + onClose: () => console.log('closed feedback form'), + focusOnLoad: false + }} + /> + alert(`Selected ${quickResponse}`), + // eslint-disable-next-line no-console + onClose: () => console.log('closed feedback form'), + focusOnLoad: false + }} + /> + + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), + focusOnLoad: false + }} + /> + console.log('closed completion message'), focusOnLoad: false }} + /> + + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedbackTimeout.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedbackTimeout.tsx new file mode 100644 index 00000000..e523b18a --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedbackTimeout.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; +import { Button } from '@patternfly/react-core'; + +export const MessageWithFeedbackTimeoutExample: React.FunctionComponent = () => { + const [hasFeedback, setHasFeedback] = React.useState(false); + + return ( + <> + + + + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), + hasTextArea: true, + timeout: true + } + : undefined + /* eslint-enable indent */ + } + isLiveRegion + /> + , + + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index bbccee81..1f699eeb 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -14,14 +14,17 @@ propComponents: [ 'AttachMenu', 'AttachmentEdit', - 'FileDetails', - 'FileDetailsLabel', + 'FileDetailsProps', + 'FileDetailsLabelProps', 'FileDropZone', 'PreviewAttachment', 'Message', 'PreviewAttachment', 'ActionProps', - 'SourcesCardProps' + 'SourcesCardProps', + 'UserFeedbackProps', + 'UserFeedbackCompleteProps', + 'QuickResponseProps' ] sortValue: 3 --- @@ -97,6 +100,32 @@ You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is ``` +### Message feedback response + +When a user selects a positive or negative [message action](#message-actions), you can choose to display a feedback card under the message. These cards can be displayed to gather additional feedback and acknowledge a response. The card will be focused on load by default, but this can be customized by setting the `focusOnLoad` prop to false. This prop is set to false in many of these examples so that focus is unaffected, but you will want to leave this on in a standard context. + +Cards can be closed manually via the close button or be configured to time out (see [below](/patternfly-ai/chatbot/messages#message-feedback-response-with-timeouts)). These examples demonstrate the full feedback flow we recommend (namely, submitting additional feedback and seeing the thank you card), just the feedback card, the feedback card without a text input, the feedback card without a close button, just the thank-you card, and the thank-you card without a close button. Additional props are available for further customization. + +The full feedback flow example also demonstrates how to handle focus appropriately for accessibility. The card will be focused when it appears in the DOM. When the card closes, place the focus back on the launching button. You can also add `aria-expanded` and `aria-controls` attributes to the feedback buttons to provide additional context on what the button controls. + +It is also important to announce when new content appears onscreen for accessibility purposes. If you set `isLiveRegion` to true on ``, it will make appropriate announcements for you when the feedback card appears. + +```js file="./MessageWithFeedback.tsx" + +``` + +### Message feedback response with timeouts + +Both feedback cards can also be configured to time out. While the card is based on the [PatternFly Card component](/components/card/), the timeout behavior and API are based on the [PatternFly Alert component](/components/alert/). The messages can be configured with different timeout durations via the `timeout` prop (default is 8000 ms). + +If a user is hovering over the card or focused on it, it will not dismiss right away. The default is 3000 ms. and it can be customized by setting the `timeoutAnimation` prop. You can also set an `onTimeout` callback and optional `onMouseEnter` and `onMouseLeave` callbacks. + +It is important to announce when new content appears onscreen for accessibility purposes. If you set `isLiveRegion` to true on ``, it will make appropriate announcements for you when the feedback card appears. + +```js file="./MessageWithFeedbackTimeout.tsx" + +``` + ### Messages with quick responses You can offer convenient, clickable responses to messages in the form of quick actions. Quick actions are [PatternFly labels](/components/label/) in a label group, configured to display up to 5 visible labels. Only 1 response can be selected at a time. diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 84ad1102..bfa247ff 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -19,6 +19,8 @@ import OrderedListMessage from './ListMessage/OrderedListMessage'; import QuickStartTile from './QuickStarts/QuickStartTile'; import { QuickStart, QuickstartAction } from './QuickStarts/types'; import QuickResponse from './QuickResponse/QuickResponse'; +import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback'; +import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete'; export interface MessageAttachment { /** Name of file attached to the message */ @@ -74,6 +76,10 @@ export interface MessageProps extends Omit, 'rol quickResponses?: QuickResponse[]; /** Props for quick responses container */ quickResponseContainerProps?: Omit; + /** Props for user feedback card */ + userFeedbackForm?: Omit; + /** Props for user feedback response */ + userFeedbackComplete?: Omit; /** Whether avatar is round */ hasRoundAvatar?: boolean; /** Any additional props applied to the avatar, for additional customization */ @@ -91,9 +97,13 @@ export interface MessageProps extends Omit, 'rol onClick?: () => void; action?: QuickstartAction; }; + /** Turns the container into a live region so that changes to content within the Message, such as appending a feedback card, are reliably announced to assistive technology. */ + isLiveRegion?: boolean; + /** Ref applied to message */ + innerRef?: React.Ref; } -export const Message: React.FunctionComponent = ({ +export const MessageBase: React.FunctionComponent = ({ role, content, name, @@ -111,6 +121,10 @@ export const Message: React.FunctionComponent = ({ hasRoundAvatar = true, avatarProps, quickStarts, + userFeedbackForm, + userFeedbackComplete, + isLiveRegion, + innerRef, ...props }: MessageProps) => { let avatarClassName; @@ -127,6 +141,9 @@ export const Message: React.FunctionComponent = ({
{/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */} @@ -181,6 +198,8 @@ export const Message: React.FunctionComponent = ({ /> )} {!isLoading && actions && } + {userFeedbackForm && } + {userFeedbackComplete && } {!isLoading && quickResponses && ( = ({ ); }; +const Message = React.forwardRef((props: MessageProps, ref: React.Ref) => ( + +)); + export default Message; diff --git a/packages/module/src/Message/QuickResponse/QuickResponse.tsx b/packages/module/src/Message/QuickResponse/QuickResponse.tsx index b476a3c2..6a043560 100644 --- a/packages/module/src/Message/QuickResponse/QuickResponse.tsx +++ b/packages/module/src/Message/QuickResponse/QuickResponse.tsx @@ -5,7 +5,7 @@ import { CheckIcon } from '@patternfly/react-icons'; export interface QuickResponse extends Omit { content: string; id: string; - onClick: () => void; + onClick?: () => void; } export interface QuickResponseProps { @@ -13,17 +13,21 @@ export interface QuickResponseProps { quickResponses: QuickResponse[]; /** Props for quick responses container */ quickResponseContainerProps?: Omit; + /** Callback when a response is clicked; used in feedback cards */ + onSelect?: (id: string) => void; } export const QuickResponse: React.FunctionComponent = ({ quickResponses, - quickResponseContainerProps = { numLabels: 5 } + quickResponseContainerProps = { numLabels: 5 }, + onSelect }: QuickResponseProps) => { const [selectedQuickResponse, setSelectedQuickResponse] = React.useState(); const handleQuickResponseClick = (id: string, onClick?: () => void) => { setSelectedQuickResponse(id); onClick && onClick(); + onSelect && onSelect(id); }; return ( void; + /** Aria-label for button */ + ariaLabel?: string; +} + +const CloseButton: React.FunctionComponent = ({ onClose, ariaLabel }: CloseButtonProps) => ( +