Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,43 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';

export const ResponseActionExample: FunctionComponent = () => (
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="I updated your account with those settings. You're ready to set up your first dashboard!"
actions={{
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
<>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="I updated your account with those settings. You're ready to set up your first dashboard!"
actions={{
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
<Message
name="Bot"
role="bot"
showActionsOnInteraction
avatar={patternflyAvatar}
content="This message has response actions visually hidden until you hover over the message via mouse, or an action would receive focus via keyboard."
actions={{
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,16 @@ For example, you can use the default divider to display a "timestamp" for more s

### Message actions

You can add actions to a message, to allow users to interact with the message content. These actions can include:
To let users interact with a bot's responses, you can add support for message actions. While you can customize message actions to your needs, default options include the following:

- Feedback responses that allow users to rate a message as "good" or "bad".
- Copy and share controls that allow users to share the message content with others.
- An edit action to allow users to edit a message they previously sent. This should only be applied to user messages - see the [user messages example](#user-messages) for details on how to implement this action.
- A listen action, that will read the message content out loud.
- Positive and negative feedback: Allows users to rate a message as "good" or "bad."
- Copy: Allows users to copy the message content to their clipboard.
- Download: Allows users to download the message content.
- Listen: Reads the message content out loud using text-to-speech.

**Note:** The logic for the actions is not built into the component and must be implemented by the consuming application.
You can display message actions by default, or use the `showActionsOnInteraction` prop to reveal actions on hover or keyboard focus.

**Note**: The underlying logic for these actions is not built-in and must be implemented within the consuming application.

```js file="./MessageWithResponseActions.tsx"

Expand Down Expand Up @@ -140,11 +142,10 @@ When `persistActionSelection` is `true`:

### Message actions that fill

To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.
To provide enhanced visual feedback when users interact with response actions, you can enable icon swapping by setting `useFilledIconsOnClick` to `true`. When enabled, the predefined "positive" and "negative" actions will automatically swap to their filled icon counterparts when clicked, replacing the original outlined icon variants.

This is especially useful for actions that are intended to persist (such as the "positive" and "negative" responses), so that a user's selection is more clear and emphasized.


```js file="./MessageWithIconSwapping.tsx"

```
Expand Down
12 changes: 12 additions & 0 deletions packages/module/src/Message/Message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@
gap: var(--pf-t--global--spacer--sm);
}

.pf-m-visible-interaction {
opacity: 0;
transition-timing-function: var(--pf-t--global--motion--timing-function--default);
transition-duration: var(--pf-t--global--motion--duration--fade--short);
transition-property: opacity;
}

&:hover .pf-m-visible-interaction,
.pf-m-visible-interaction:focus-within {
opacity: 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want any sort of transition token to allow this to fade in/out or anything like that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! let use pf-t--global--motion--duration--fade--short

}

// targets footnotes specifically
.footnotes,
.pf-chatbot__message-text.footnotes {
Expand Down
136 changes: 136 additions & 0 deletions packages/module/src/Message/Message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1415,4 +1415,140 @@ describe('Message', () => {
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
});

it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
showActionsOnInteraction
actions={{
positive: { onClick: jest.fn() }
}}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions');
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
showActionsOnInteraction={false}
actions={{
positive: { onClick: jest.fn() }
}}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions');
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class to response actions by default', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
actions={{
positive: { onClick: jest.fn() }
}}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions');
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
});

it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
showActionsOnInteraction
actions={[
{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
},
{
copy: { onClick: jest.fn() }
}
]}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions-groups');
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
showActionsOnInteraction={false}
actions={[
{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
},
{
copy: { onClick: jest.fn() }
}
]}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions-groups');
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
render(
<Message
avatar="./img"
role="bot"
name="Bot"
content="Hi"
actions={[
{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
},
{
copy: { onClick: jest.fn() }
}
]}
/>
);

const responseContainer = screen
.getByRole('button', { name: 'Good response' })
.closest('.pf-chatbot__response-actions-groups');
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
});
});
13 changes: 12 additions & 1 deletion packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
* For finer control of multiple action groups, use persistActionSelection on each group.
*/
persistActionSelection?: boolean;
/** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
* that setting this to true will append tooltips inline instead of the document.body.
*/
showActionsOnInteraction?: boolean;
/** Sources for message */
sources?: SourcesCardProps;
/** Label for the English word "AI," used to tag messages with role "bot" */
Expand Down Expand Up @@ -214,6 +218,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
isLoading,
actions,
persistActionSelection,
showActionsOnInteraction = false,
sources,
botWord = 'AI',
loadingWord = 'Loading message',
Expand Down Expand Up @@ -382,7 +387,12 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
{!isLoading && !isEditable && actions && (
<>
{Array.isArray(actions) ? (
<div className="pf-chatbot__response-actions-groups">
<div
className={css(
'pf-chatbot__response-actions-groups',
showActionsOnInteraction && 'pf-m-visible-interaction'
)}
>
{actions.map((actionGroup, index) => (
<ResponseActions
key={index}
Expand All @@ -397,6 +407,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
actions={actions}
persistActionSelection={persistActionSelection}
useFilledIconsOnClick={useFilledIconsOnClick}
showActionsOnInteraction={showActionsOnInteraction}
/>
)}
</>
Expand Down
59 changes: 59 additions & 0 deletions packages/module/src/ResponseActions/ResponseActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,65 @@ describe('ResponseActions', () => {
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
});

it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
render(
<ResponseActions
data-testid="test-id"
actions={{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
}}
showActionsOnInteraction
/>
);

expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
render(
<ResponseActions
data-testid="test-id"
actions={{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
}}
showActionsOnInteraction={false}
/>
);

expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
});

it('should not apply pf-m-visible-interaction class by default', () => {
render(
<ResponseActions
data-testid="test-id"
actions={{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
}}
/>
);

expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
});

it('should render with custom className', () => {
render(
<ResponseActions
data-testid="test-id"
actions={{
positive: { onClick: jest.fn() },
negative: { onClick: jest.fn() }
}}
className="custom-class"
/>
);

expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
});

describe('icon swapping with useFilledIconsOnClick', () => {
it('should render outline icons by default', () => {
render(
Expand Down
Loading
Loading