Skip to content

Commit 45c7788

Browse files
Merge pull request #806 from thatblindgeye/iss792_hoverActions
feat(ResponseActions): added opt-in for hiding actions until interaction
2 parents 56e929c + e990e3f commit 45c7788

File tree

7 files changed

+302
-37
lines changed

7 files changed

+302
-37
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,43 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
44
import patternflyAvatar from './patternfly_avatar.jpg';
55

66
export const ResponseActionExample: FunctionComponent = () => (
7-
<Message
8-
name="Bot"
9-
role="bot"
10-
avatar={patternflyAvatar}
11-
content="I updated your account with those settings. You're ready to set up your first dashboard!"
12-
actions={{
13-
// eslint-disable-next-line no-console
14-
positive: { onClick: () => console.log('Good response') },
15-
// eslint-disable-next-line no-console
16-
negative: { onClick: () => console.log('Bad response') },
17-
// eslint-disable-next-line no-console
18-
copy: { onClick: () => console.log('Copy') },
19-
// eslint-disable-next-line no-console
20-
download: { onClick: () => console.log('Download') },
21-
// eslint-disable-next-line no-console
22-
listen: { onClick: () => console.log('Listen') }
23-
}}
24-
/>
7+
<>
8+
<Message
9+
name="Bot"
10+
role="bot"
11+
avatar={patternflyAvatar}
12+
content="I updated your account with those settings. You're ready to set up your first dashboard!"
13+
actions={{
14+
// eslint-disable-next-line no-console
15+
positive: { onClick: () => console.log('Good response') },
16+
// eslint-disable-next-line no-console
17+
negative: { onClick: () => console.log('Bad response') },
18+
// eslint-disable-next-line no-console
19+
copy: { onClick: () => console.log('Copy') },
20+
// eslint-disable-next-line no-console
21+
download: { onClick: () => console.log('Download') },
22+
// eslint-disable-next-line no-console
23+
listen: { onClick: () => console.log('Listen') }
24+
}}
25+
/>
26+
<Message
27+
name="Bot"
28+
role="bot"
29+
showActionsOnInteraction
30+
avatar={patternflyAvatar}
31+
content="This message has response actions visually hidden until you hover over the message via mouse, or an action would receive focus via keyboard."
32+
actions={{
33+
// eslint-disable-next-line no-console
34+
positive: { onClick: () => console.log('Good response') },
35+
// eslint-disable-next-line no-console
36+
negative: { onClick: () => console.log('Bad response') },
37+
// eslint-disable-next-line no-console
38+
copy: { onClick: () => console.log('Copy') },
39+
// eslint-disable-next-line no-console
40+
download: { onClick: () => console.log('Download') },
41+
// eslint-disable-next-line no-console
42+
listen: { onClick: () => console.log('Listen') }
43+
}}
44+
/>
45+
</>
2546
);

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ For example, you can use the default divider to display a "timestamp" for more s
9595

9696
### Message actions
9797

98-
You can add actions to a message, to allow users to interact with the message content. These actions can include:
98+
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:
9999

100-
- Feedback responses that allow users to rate a message as "good" or "bad".
101-
- Copy and share controls that allow users to share the message content with others.
102-
- 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.
103-
- A listen action, that will read the message content out loud.
100+
- Positive and negative feedback: Allows users to rate a message as "good" or "bad."
101+
- Copy: Allows users to copy the message content to their clipboard.
102+
- Download: Allows users to download the message content.
103+
- Listen: Reads the message content out loud using text-to-speech.
104104

105-
**Note:** The logic for the actions is not built into the component and must be implemented by the consuming application.
105+
You can display message actions by default, or use the `showActionsOnInteraction` prop to reveal actions on hover or keyboard focus.
106+
107+
**Note**: The underlying logic for these actions is not built-in and must be implemented within the consuming application.
106108

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

@@ -140,11 +142,10 @@ When `persistActionSelection` is `true`:
140142

141143
### Message actions that fill
142144

143-
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.
145+
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.
144146

145147
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.
146148

147-
148149
```js file="./MessageWithIconSwapping.tsx"
149150

150151
```

packages/module/src/Message/Message.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@
9292
gap: var(--pf-t--global--spacer--sm);
9393
}
9494

95+
.pf-m-visible-interaction {
96+
opacity: 0;
97+
transition-timing-function: var(--pf-t--global--motion--timing-function--default);
98+
transition-duration: var(--pf-t--global--motion--duration--fade--short);
99+
transition-property: opacity;
100+
}
101+
102+
&:hover .pf-m-visible-interaction,
103+
.pf-m-visible-interaction:focus-within {
104+
opacity: 1;
105+
}
106+
95107
// targets footnotes specifically
96108
.footnotes,
97109
.pf-chatbot__message-text.footnotes {

packages/module/src/Message/Message.test.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,4 +1415,140 @@ describe('Message', () => {
14151415
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
14161416
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
14171417
});
1418+
1419+
it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
1420+
render(
1421+
<Message
1422+
avatar="./img"
1423+
role="bot"
1424+
name="Bot"
1425+
content="Hi"
1426+
showActionsOnInteraction
1427+
actions={{
1428+
positive: { onClick: jest.fn() }
1429+
}}
1430+
/>
1431+
);
1432+
1433+
const responseContainer = screen
1434+
.getByRole('button', { name: 'Good response' })
1435+
.closest('.pf-chatbot__response-actions');
1436+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1437+
});
1438+
1439+
it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
1440+
render(
1441+
<Message
1442+
avatar="./img"
1443+
role="bot"
1444+
name="Bot"
1445+
content="Hi"
1446+
showActionsOnInteraction={false}
1447+
actions={{
1448+
positive: { onClick: jest.fn() }
1449+
}}
1450+
/>
1451+
);
1452+
1453+
const responseContainer = screen
1454+
.getByRole('button', { name: 'Good response' })
1455+
.closest('.pf-chatbot__response-actions');
1456+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1457+
});
1458+
1459+
it('should not apply pf-m-visible-interaction class to response actions by default', () => {
1460+
render(
1461+
<Message
1462+
avatar="./img"
1463+
role="bot"
1464+
name="Bot"
1465+
content="Hi"
1466+
actions={{
1467+
positive: { onClick: jest.fn() }
1468+
}}
1469+
/>
1470+
);
1471+
1472+
const responseContainer = screen
1473+
.getByRole('button', { name: 'Good response' })
1474+
.closest('.pf-chatbot__response-actions');
1475+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1476+
});
1477+
1478+
it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
1479+
render(
1480+
<Message
1481+
avatar="./img"
1482+
role="bot"
1483+
name="Bot"
1484+
content="Hi"
1485+
showActionsOnInteraction
1486+
actions={[
1487+
{
1488+
positive: { onClick: jest.fn() },
1489+
negative: { onClick: jest.fn() }
1490+
},
1491+
{
1492+
copy: { onClick: jest.fn() }
1493+
}
1494+
]}
1495+
/>
1496+
);
1497+
1498+
const responseContainer = screen
1499+
.getByRole('button', { name: 'Good response' })
1500+
.closest('.pf-chatbot__response-actions-groups');
1501+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1502+
});
1503+
1504+
it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
1505+
render(
1506+
<Message
1507+
avatar="./img"
1508+
role="bot"
1509+
name="Bot"
1510+
content="Hi"
1511+
showActionsOnInteraction={false}
1512+
actions={[
1513+
{
1514+
positive: { onClick: jest.fn() },
1515+
negative: { onClick: jest.fn() }
1516+
},
1517+
{
1518+
copy: { onClick: jest.fn() }
1519+
}
1520+
]}
1521+
/>
1522+
);
1523+
1524+
const responseContainer = screen
1525+
.getByRole('button', { name: 'Good response' })
1526+
.closest('.pf-chatbot__response-actions-groups');
1527+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1528+
});
1529+
1530+
it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
1531+
render(
1532+
<Message
1533+
avatar="./img"
1534+
role="bot"
1535+
name="Bot"
1536+
content="Hi"
1537+
actions={[
1538+
{
1539+
positive: { onClick: jest.fn() },
1540+
negative: { onClick: jest.fn() }
1541+
},
1542+
{
1543+
copy: { onClick: jest.fn() }
1544+
}
1545+
]}
1546+
/>
1547+
);
1548+
1549+
const responseContainer = screen
1550+
.getByRole('button', { name: 'Good response' })
1551+
.closest('.pf-chatbot__response-actions-groups');
1552+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1553+
});
14181554
});

packages/module/src/Message/Message.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
114114
* For finer control of multiple action groups, use persistActionSelection on each group.
115115
*/
116116
persistActionSelection?: boolean;
117+
/** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
118+
* that setting this to true will append tooltips inline instead of the document.body.
119+
*/
120+
showActionsOnInteraction?: boolean;
117121
/** Sources for message */
118122
sources?: SourcesCardProps;
119123
/** Label for the English word "AI," used to tag messages with role "bot" */
@@ -214,6 +218,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
214218
isLoading,
215219
actions,
216220
persistActionSelection,
221+
showActionsOnInteraction = false,
217222
sources,
218223
botWord = 'AI',
219224
loadingWord = 'Loading message',
@@ -382,7 +387,12 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
382387
{!isLoading && !isEditable && actions && (
383388
<>
384389
{Array.isArray(actions) ? (
385-
<div className="pf-chatbot__response-actions-groups">
390+
<div
391+
className={css(
392+
'pf-chatbot__response-actions-groups',
393+
showActionsOnInteraction && 'pf-m-visible-interaction'
394+
)}
395+
>
386396
{actions.map((actionGroup, index) => (
387397
<ResponseActions
388398
key={index}
@@ -397,6 +407,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
397407
actions={actions}
398408
persistActionSelection={persistActionSelection}
399409
useFilledIconsOnClick={useFilledIconsOnClick}
410+
showActionsOnInteraction={showActionsOnInteraction}
400411
/>
401412
)}
402413
</>

packages/module/src/ResponseActions/ResponseActions.test.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,65 @@ describe('ResponseActions', () => {
437437
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
438438
});
439439

440+
it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
441+
render(
442+
<ResponseActions
443+
data-testid="test-id"
444+
actions={{
445+
positive: { onClick: jest.fn() },
446+
negative: { onClick: jest.fn() }
447+
}}
448+
showActionsOnInteraction
449+
/>
450+
);
451+
452+
expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
453+
});
454+
455+
it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
456+
render(
457+
<ResponseActions
458+
data-testid="test-id"
459+
actions={{
460+
positive: { onClick: jest.fn() },
461+
negative: { onClick: jest.fn() }
462+
}}
463+
showActionsOnInteraction={false}
464+
/>
465+
);
466+
467+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
468+
});
469+
470+
it('should not apply pf-m-visible-interaction class by default', () => {
471+
render(
472+
<ResponseActions
473+
data-testid="test-id"
474+
actions={{
475+
positive: { onClick: jest.fn() },
476+
negative: { onClick: jest.fn() }
477+
}}
478+
/>
479+
);
480+
481+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
482+
});
483+
484+
it('should render with custom className', () => {
485+
render(
486+
<ResponseActions
487+
data-testid="test-id"
488+
actions={{
489+
positive: { onClick: jest.fn() },
490+
negative: { onClick: jest.fn() }
491+
}}
492+
className="custom-class"
493+
/>
494+
);
495+
496+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
497+
});
498+
440499
describe('icon swapping with useFilledIconsOnClick', () => {
441500
it('should render outline icons by default', () => {
442501
render(

0 commit comments

Comments
 (0)