Skip to content

Commit 337ae03

Browse files
committed
feat(ResponseActions): added opt-in for hiding actions until interaction
1 parent bb287cc commit 337ae03

File tree

6 files changed

+296
-30
lines changed

6 files changed

+296
-30
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/src/Message/Message.scss

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

95+
.pf-m-visible-interaction {
96+
opacity: 0;
97+
}
98+
99+
&:hover .pf-m-visible-interaction,
100+
.pf-m-visible-interaction:focus-within {
101+
opacity: 1;
102+
}
103+
95104
// targets footnotes specifically
96105
.footnotes,
97106
.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
@@ -1351,4 +1351,140 @@ describe('Message', () => {
13511351
render(<Message alignment="end" avatar="./img" role="user" name="User" content="" />);
13521352
expect(screen.getByRole('region')).toHaveClass('pf-m-end');
13531353
});
1354+
1355+
it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
1356+
render(
1357+
<Message
1358+
avatar="./img"
1359+
role="bot"
1360+
name="Bot"
1361+
content="Hi"
1362+
showActionsOnInteraction
1363+
actions={{
1364+
positive: { onClick: jest.fn() }
1365+
}}
1366+
/>
1367+
);
1368+
1369+
const responseContainer = screen
1370+
.getByRole('button', { name: 'Good response' })
1371+
.closest('.pf-chatbot__response-actions');
1372+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1373+
});
1374+
1375+
it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
1376+
render(
1377+
<Message
1378+
avatar="./img"
1379+
role="bot"
1380+
name="Bot"
1381+
content="Hi"
1382+
showActionsOnInteraction={false}
1383+
actions={{
1384+
positive: { onClick: jest.fn() }
1385+
}}
1386+
/>
1387+
);
1388+
1389+
const responseContainer = screen
1390+
.getByRole('button', { name: 'Good response' })
1391+
.closest('.pf-chatbot__response-actions');
1392+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1393+
});
1394+
1395+
it('should not apply pf-m-visible-interaction class to response actions by default', () => {
1396+
render(
1397+
<Message
1398+
avatar="./img"
1399+
role="bot"
1400+
name="Bot"
1401+
content="Hi"
1402+
actions={{
1403+
positive: { onClick: jest.fn() }
1404+
}}
1405+
/>
1406+
);
1407+
1408+
const responseContainer = screen
1409+
.getByRole('button', { name: 'Good response' })
1410+
.closest('.pf-chatbot__response-actions');
1411+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1412+
});
1413+
1414+
it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
1415+
render(
1416+
<Message
1417+
avatar="./img"
1418+
role="bot"
1419+
name="Bot"
1420+
content="Hi"
1421+
showActionsOnInteraction
1422+
actions={[
1423+
{
1424+
positive: { onClick: jest.fn() },
1425+
negative: { onClick: jest.fn() }
1426+
},
1427+
{
1428+
copy: { onClick: jest.fn() }
1429+
}
1430+
]}
1431+
/>
1432+
);
1433+
1434+
const responseContainer = screen
1435+
.getByRole('button', { name: 'Good response' })
1436+
.closest('.pf-chatbot__response-actions-groups');
1437+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1438+
});
1439+
1440+
it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
1441+
render(
1442+
<Message
1443+
avatar="./img"
1444+
role="bot"
1445+
name="Bot"
1446+
content="Hi"
1447+
showActionsOnInteraction={false}
1448+
actions={[
1449+
{
1450+
positive: { onClick: jest.fn() },
1451+
negative: { onClick: jest.fn() }
1452+
},
1453+
{
1454+
copy: { onClick: jest.fn() }
1455+
}
1456+
]}
1457+
/>
1458+
);
1459+
1460+
const responseContainer = screen
1461+
.getByRole('button', { name: 'Good response' })
1462+
.closest('.pf-chatbot__response-actions-groups');
1463+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1464+
});
1465+
1466+
it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
1467+
render(
1468+
<Message
1469+
avatar="./img"
1470+
role="bot"
1471+
name="Bot"
1472+
content="Hi"
1473+
actions={[
1474+
{
1475+
positive: { onClick: jest.fn() },
1476+
negative: { onClick: jest.fn() }
1477+
},
1478+
{
1479+
copy: { onClick: jest.fn() }
1480+
}
1481+
]}
1482+
/>
1483+
);
1484+
1485+
const responseContainer = screen
1486+
.getByRole('button', { name: 'Good response' })
1487+
.closest('.pf-chatbot__response-actions-groups');
1488+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1489+
});
13541490
});

packages/module/src/Message/Message.tsx

Lines changed: 16 additions & 2 deletions
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" */
@@ -212,6 +216,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
212216
isLoading,
213217
actions,
214218
persistActionSelection,
219+
showActionsOnInteraction = false,
215220
sources,
216221
botWord = 'AI',
217222
loadingWord = 'Loading message',
@@ -379,7 +384,12 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
379384
{!isLoading && !isEditable && actions && (
380385
<>
381386
{Array.isArray(actions) ? (
382-
<div className="pf-chatbot__response-actions-groups">
387+
<div
388+
className={css(
389+
'pf-chatbot__response-actions-groups',
390+
showActionsOnInteraction && 'pf-m-visible-interaction'
391+
)}
392+
>
383393
{actions.map((actionGroup, index) => (
384394
<ResponseActions
385395
key={index}
@@ -389,7 +399,11 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
389399
))}
390400
</div>
391401
) : (
392-
<ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
402+
<ResponseActions
403+
actions={actions}
404+
persistActionSelection={persistActionSelection}
405+
showActionsOnInteraction={showActionsOnInteraction}
406+
/>
393407
)}
394408
</>
395409
)}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,63 @@ describe('ResponseActions', () => {
421421
await userEvent.click(customBtn);
422422
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
423423
});
424+
425+
it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
426+
render(
427+
<ResponseActions
428+
data-testid="test-id"
429+
actions={{
430+
positive: { onClick: jest.fn() },
431+
negative: { onClick: jest.fn() }
432+
}}
433+
showActionsOnInteraction
434+
/>
435+
);
436+
437+
expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
438+
});
439+
440+
it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
441+
render(
442+
<ResponseActions
443+
data-testid="test-id"
444+
actions={{
445+
positive: { onClick: jest.fn() },
446+
negative: { onClick: jest.fn() }
447+
}}
448+
showActionsOnInteraction={false}
449+
/>
450+
);
451+
452+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
453+
});
454+
455+
it('should not apply pf-m-visible-interaction class by default', () => {
456+
render(
457+
<ResponseActions
458+
data-testid="test-id"
459+
actions={{
460+
positive: { onClick: jest.fn() },
461+
negative: { onClick: jest.fn() }
462+
}}
463+
/>
464+
);
465+
466+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
467+
});
468+
469+
it('should render with custom className', () => {
470+
render(
471+
<ResponseActions
472+
data-testid="test-id"
473+
actions={{
474+
positive: { onClick: jest.fn() },
475+
negative: { onClick: jest.fn() }
476+
}}
477+
className="custom-class"
478+
/>
479+
);
480+
481+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
482+
});
424483
});

0 commit comments

Comments
 (0)