Skip to content
Merged
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"
Comment thread
rebeccaalpert marked this conversation as resolved.
Outdated
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,40 @@ You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is

```

### Message feedback

When a user selects a positive or negative [message action](#message-actions), you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button or be [configured to time out automatically](/patternfly-ai/chatbot/messages#message-feedback-with-timeouts).
<br /><br />
Comment thread
rebeccaalpert marked this conversation as resolved.
Outdated
The message feedback card will immediately receive focus by default, but you remove this behavior by passing `focusOnLoad: false` to the `<Message>` (as shown in the following examples). For better usability, you should generally keep the default focus behavior.
<br /><br />
The following examples demonstrate:

- A full feedback flow, which accepts written feedback submission and displays the thank you card.
- A basic card.
- A basic card without text input.
- A card without a close button.
- Thank-you cards, with and without a close button.

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.
<br /><br />
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 with timeouts

The feedback card and thank you message can be configured to time out and automatically close after a period of time. The default time out period is 8000 ms, but it can be customized via `timeout`.
<br /><br />
The card will not dismiss within the default time if a user is hovering over it or if it has keyboard focus. Instead, it will dismiss after they remove focus, via `timeoutAnimation`, which is 3000 ms by default. You can adjust this duration and set an `onTimeout` callback, as well as optional `onMouseEnter` and `onMouseLeave` callbacks.
<br /><br />
For accessibility purposes, be sure to announce when new content appears onscreen. If you set `isLiveRegion` to `true` for a `<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