Skip to content

Commit

Permalink
feat(UserFeedback): Add user feedback cards
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccaalpert committed Jan 15, 2025
1 parent ef74c6b commit 22cfcd3
Show file tree
Hide file tree
Showing 14 changed files with 1,419 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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<string>();
const positiveRef = React.useRef<HTMLButtonElement>(null);
const negativeRef = React.useRef<HTMLButtonElement>(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 (
<>
<Message
isLiveRegion
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with user feedback flow; click on a message action to launch the feedback flow. Click submit to see the thank you message."
actions={{
positive: {
onClick: () => {
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 */
}
/>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with feedback form only"
userFeedbackForm={{
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}`),
hasTextArea: true,
// eslint-disable-next-line no-console
onClose: () => console.log('closed feedback form'),
focusOnLoad: false
}}
/>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with feedback form that doesn't include text area"
userFeedbackForm={{
quickResponses: [
{ id: '1', content: 'Correct' },
{ id: '2', content: 'Easy to understand' },
{ id: '3', content: 'Complete' }
],
onSubmit: (quickResponse) => alert(`Selected ${quickResponse}`),
// eslint-disable-next-line no-console
onClose: () => console.log('closed feedback form'),
focusOnLoad: false
}}
/>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with feedback form without close button"
userFeedbackForm={{
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}`),
focusOnLoad: false
}}
/>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with completion message"
// eslint-disable-next-line no-console
userFeedbackComplete={{ onClose: () => console.log('closed completion message'), focusOnLoad: false }}
/>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with completion message without close button"
userFeedbackComplete={{ focusOnLoad: false }}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Button variant="secondary" onClick={() => setHasFeedback(true)}>
Show feedback cards
</Button>
<Button variant="secondary" onClick={() => setHasFeedback(false)}>
Remove all feedback cards
</Button>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with feedback form that times out"
userFeedbackForm={
/* eslint-disable indent */
hasFeedback
? {
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}`),
hasTextArea: true,
timeout: true
}
: undefined
/* eslint-enable indent */
}
isLiveRegion
/>
,
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with completion message that times out"
userFeedbackComplete={hasFeedback ? { timeout: true } : undefined}
isLiveRegion
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ propComponents:
[
'AttachMenu',
'AttachmentEdit',
'FileDetails',
'FileDetailsLabel',
'FileDetailsProps',
'FileDetailsLabelProps',
'FileDropZone',
'PreviewAttachment',
'Message',
'PreviewAttachment',
'ActionProps',
'SourcesCardProps'
'SourcesCardProps',
'UserFeedbackProps',
'UserFeedbackCompleteProps',
'QuickResponseProps'
]
sortValue: 3
---
Expand Down Expand Up @@ -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 `<Message>`, 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 `<Message>`, 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.
Expand Down
25 changes: 24 additions & 1 deletion packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -74,6 +76,10 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
quickResponses?: QuickResponse[];
/** Props for quick responses container */
quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
/** Props for user feedback card */
userFeedbackForm?: Omit<UserFeedbackProps, 'ref'>;
/** Props for user feedback response */
userFeedbackComplete?: Omit<UserFeedbackCompleteProps, 'ref'>;
/** Whether avatar is round */
hasRoundAvatar?: boolean;
/** Any additional props applied to the avatar, for additional customization */
Expand All @@ -91,9 +97,13 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, '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<HTMLDivElement>;
}

export const Message: React.FunctionComponent<MessageProps> = ({
export const MessageBase: React.FunctionComponent<MessageProps> = ({
role,
content,
name,
Expand All @@ -111,6 +121,10 @@ export const Message: React.FunctionComponent<MessageProps> = ({
hasRoundAvatar = true,
avatarProps,
quickStarts,
userFeedbackForm,
userFeedbackComplete,
isLiveRegion,
innerRef,
...props
}: MessageProps) => {
let avatarClassName;
Expand All @@ -127,6 +141,9 @@ export const Message: React.FunctionComponent<MessageProps> = ({
<section
aria-label={`Message from ${role} - ${dateString}`}
className={`pf-chatbot__message pf-chatbot__message--${role}`}
aria-live={isLiveRegion ? 'polite' : undefined}
aria-atomic={isLiveRegion ? false : undefined}
ref={innerRef}
{...props}
>
{/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */}
Expand Down Expand Up @@ -181,6 +198,8 @@ export const Message: React.FunctionComponent<MessageProps> = ({
/>
)}
{!isLoading && actions && <ResponseActions actions={actions} />}
{userFeedbackForm && <UserFeedback {...userFeedbackForm} />}
{userFeedbackComplete && <UserFeedbackComplete {...userFeedbackComplete} />}
{!isLoading && quickResponses && (
<QuickResponse
quickResponses={quickResponses}
Expand Down Expand Up @@ -212,4 +231,8 @@ export const Message: React.FunctionComponent<MessageProps> = ({
);
};

const Message = React.forwardRef((props: MessageProps, ref: React.Ref<HTMLDivElement>) => (
<MessageBase innerRef={ref} {...props} />
));

export default Message;
8 changes: 6 additions & 2 deletions packages/module/src/Message/QuickResponse/QuickResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@ import { CheckIcon } from '@patternfly/react-icons';
export interface QuickResponse extends Omit<LabelProps, 'children'> {
content: string;
id: string;
onClick: () => void;
onClick?: () => void;
}

export interface QuickResponseProps {
/** Props for quick responses */
quickResponses: QuickResponse[];
/** Props for quick responses container */
quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
/** Callback when a response is clicked; used in feedback cards */
onSelect?: (id: string) => void;
}

export const QuickResponse: React.FunctionComponent<QuickResponseProps> = ({
quickResponses,
quickResponseContainerProps = { numLabels: 5 }
quickResponseContainerProps = { numLabels: 5 },
onSelect
}: QuickResponseProps) => {
const [selectedQuickResponse, setSelectedQuickResponse] = React.useState<string>();

const handleQuickResponseClick = (id: string, onClick?: () => void) => {
setSelectedQuickResponse(id);
onClick && onClick();
onSelect && onSelect(id);
};
return (
<LabelGroup
Expand Down
Loading

0 comments on commit 22cfcd3

Please sign in to comment.