@patternfly/chatbot 6.6.0-prerelease.4 → 6.6.0-prerelease.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Message/Message.d.ts +6 -0
- package/dist/cjs/Message/Message.js +2 -2
- package/dist/cjs/Message/Message.test.js +109 -0
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +9 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +30 -5
- package/dist/cjs/ResponseActions/ResponseActions.test.js +134 -0
- package/dist/css/main.css +10 -0
- package/dist/css/main.css.map +1 -1
- package/dist/esm/Message/Message.d.ts +6 -0
- package/dist/esm/Message/Message.js +2 -2
- package/dist/esm/Message/Message.test.js +109 -0
- package/dist/esm/ResponseActions/ResponseActions.d.ts +9 -0
- package/dist/esm/ResponseActions/ResponseActions.js +31 -6
- package/dist/esm/ResponseActions/ResponseActions.test.js +135 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithIconSwapping.tsx +22 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx +39 -18
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +18 -6
- package/src/Message/Message.scss +12 -0
- package/src/Message/Message.test.tsx +200 -0
- package/src/Message/Message.tsx +21 -2
- package/src/ResponseActions/ResponseActions.test.tsx +259 -0
- package/src/ResponseActions/ResponseActions.tsx +65 -12
|
@@ -17,6 +17,22 @@ import { monitorSampleAppQuickStart } from './QuickStarts/monitor-sampleapp-quic
|
|
|
17
17
|
import { monitorSampleAppQuickStartWithImage } from './QuickStarts/monitor-sampleapp-quickstart-with-image';
|
|
18
18
|
import rehypeExternalLinks from '../__mocks__/rehype-external-links';
|
|
19
19
|
import { AlertActionLink, Button, CodeBlockAction } from '@patternfly/react-core';
|
|
20
|
+
// Mock the icon components
|
|
21
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
22
|
+
OutlinedThumbsUpIcon: () => _jsx("div", { children: "OutlinedThumbsUpIcon" }),
|
|
23
|
+
ThumbsUpIcon: () => _jsx("div", { children: "ThumbsUpIcon" }),
|
|
24
|
+
OutlinedThumbsDownIcon: () => _jsx("div", { children: "OutlinedThumbsDownIcon" }),
|
|
25
|
+
ThumbsDownIcon: () => _jsx("div", { children: "ThumbsDownIcon" }),
|
|
26
|
+
OutlinedCopyIcon: () => _jsx("div", { children: "OutlinedCopyIcon" }),
|
|
27
|
+
DownloadIcon: () => _jsx("div", { children: "DownloadIcon" }),
|
|
28
|
+
ExternalLinkAltIcon: () => _jsx("div", { children: "ExternalLinkAltIcon" }),
|
|
29
|
+
VolumeUpIcon: () => _jsx("div", { children: "VolumeUpIcon" }),
|
|
30
|
+
PencilAltIcon: () => _jsx("div", { children: "PencilAltIcon" }),
|
|
31
|
+
CheckIcon: () => _jsx("div", { children: "CheckIcon" }),
|
|
32
|
+
CloseIcon: () => _jsx("div", { children: "CloseIcon" }),
|
|
33
|
+
ExternalLinkSquareAltIcon: () => _jsx("div", { children: "ExternalLinkSquareAltIcon" }),
|
|
34
|
+
TimesIcon: () => _jsx("div", { children: "TimesIcon" })
|
|
35
|
+
}));
|
|
20
36
|
const ALL_ACTIONS = [
|
|
21
37
|
{ label: /Good response/i },
|
|
22
38
|
{ label: /Bad response/i },
|
|
@@ -999,4 +1015,97 @@ describe('Message', () => {
|
|
|
999
1015
|
render(_jsx(Message, { alignment: "end", avatar: "./img", role: "user", name: "User", content: "" }));
|
|
1000
1016
|
expect(screen.getByRole('region')).toHaveClass('pf-m-end');
|
|
1001
1017
|
});
|
|
1018
|
+
// We're just testing the positive action here to ensure logic passes through as needed, the other actions are
|
|
1019
|
+
// tested in ResponseActions.test.tsx along with other aspects of this functionality
|
|
1020
|
+
it('should not swap icons when useFilledIconsOnClick is omitted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1021
|
+
const user = userEvent.setup();
|
|
1022
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1023
|
+
positive: { onClick: jest.fn() }
|
|
1024
|
+
} }));
|
|
1025
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1026
|
+
yield user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1027
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
1028
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
1029
|
+
}));
|
|
1030
|
+
it('should swap icons when useFilledIconsOnClick is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1031
|
+
const user = userEvent.setup();
|
|
1032
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1033
|
+
positive: { onClick: jest.fn() }
|
|
1034
|
+
}, useFilledIconsOnClick: true }));
|
|
1035
|
+
yield user.click(screen.getByRole('button', { name: /Good response/i }));
|
|
1036
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
1037
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
1038
|
+
}));
|
|
1039
|
+
it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
|
|
1040
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: {
|
|
1041
|
+
positive: { onClick: jest.fn() }
|
|
1042
|
+
} }));
|
|
1043
|
+
const responseContainer = screen
|
|
1044
|
+
.getByRole('button', { name: 'Good response' })
|
|
1045
|
+
.closest('.pf-chatbot__response-actions');
|
|
1046
|
+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
|
|
1047
|
+
});
|
|
1048
|
+
it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
|
|
1049
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: {
|
|
1050
|
+
positive: { onClick: jest.fn() }
|
|
1051
|
+
} }));
|
|
1052
|
+
const responseContainer = screen
|
|
1053
|
+
.getByRole('button', { name: 'Good response' })
|
|
1054
|
+
.closest('.pf-chatbot__response-actions');
|
|
1055
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1056
|
+
});
|
|
1057
|
+
it('should not apply pf-m-visible-interaction class to response actions by default', () => {
|
|
1058
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: {
|
|
1059
|
+
positive: { onClick: jest.fn() }
|
|
1060
|
+
} }));
|
|
1061
|
+
const responseContainer = screen
|
|
1062
|
+
.getByRole('button', { name: 'Good response' })
|
|
1063
|
+
.closest('.pf-chatbot__response-actions');
|
|
1064
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1065
|
+
});
|
|
1066
|
+
it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
|
|
1067
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: true, actions: [
|
|
1068
|
+
{
|
|
1069
|
+
positive: { onClick: jest.fn() },
|
|
1070
|
+
negative: { onClick: jest.fn() }
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
copy: { onClick: jest.fn() }
|
|
1074
|
+
}
|
|
1075
|
+
] }));
|
|
1076
|
+
const responseContainer = screen
|
|
1077
|
+
.getByRole('button', { name: 'Good response' })
|
|
1078
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1079
|
+
expect(responseContainer).toHaveClass('pf-m-visible-interaction');
|
|
1080
|
+
});
|
|
1081
|
+
it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
|
|
1082
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", showActionsOnInteraction: false, actions: [
|
|
1083
|
+
{
|
|
1084
|
+
positive: { onClick: jest.fn() },
|
|
1085
|
+
negative: { onClick: jest.fn() }
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
copy: { onClick: jest.fn() }
|
|
1089
|
+
}
|
|
1090
|
+
] }));
|
|
1091
|
+
const responseContainer = screen
|
|
1092
|
+
.getByRole('button', { name: 'Good response' })
|
|
1093
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1094
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1095
|
+
});
|
|
1096
|
+
it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
|
|
1097
|
+
render(_jsx(Message, { avatar: "./img", role: "bot", name: "Bot", content: "Hi", actions: [
|
|
1098
|
+
{
|
|
1099
|
+
positive: { onClick: jest.fn() },
|
|
1100
|
+
negative: { onClick: jest.fn() }
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
copy: { onClick: jest.fn() }
|
|
1104
|
+
}
|
|
1105
|
+
] }));
|
|
1106
|
+
const responseContainer = screen
|
|
1107
|
+
.getByRole('button', { name: 'Good response' })
|
|
1108
|
+
.closest('.pf-chatbot__response-actions-groups');
|
|
1109
|
+
expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
|
|
1110
|
+
});
|
|
1002
1111
|
});
|
|
@@ -34,6 +34,8 @@ type ExtendedActionProps = ActionProps & {
|
|
|
34
34
|
* Use this component when passing children to Message to customize its structure.
|
|
35
35
|
*/
|
|
36
36
|
export interface ResponseActionProps {
|
|
37
|
+
/** Additional classes for the response actions container. */
|
|
38
|
+
className?: string;
|
|
37
39
|
/** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */
|
|
38
40
|
actions: Record<string, ExtendedActionProps | undefined> & {
|
|
39
41
|
positive?: ActionProps;
|
|
@@ -47,6 +49,13 @@ export interface ResponseActionProps {
|
|
|
47
49
|
/** When true, the selected action will persist even when clicking outside the component.
|
|
48
50
|
* When false (default), clicking outside or clicking another action will deselect the current selection. */
|
|
49
51
|
persistActionSelection?: boolean;
|
|
52
|
+
/** When true, automatically swaps to filled icon variants when predefined actions are clicked.
|
|
53
|
+
* Predefined actions will use filled variants (e.g., ThumbsUpIcon) when clicked and outline variants (e.g., OutlinedThumbsUpIcon) when not clicked. */
|
|
54
|
+
useFilledIconsOnClick?: boolean;
|
|
55
|
+
/** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
|
|
56
|
+
* that setting this to true will append tooltips inline instead of the document.body.
|
|
57
|
+
*/
|
|
58
|
+
showActionsOnInteraction?: boolean;
|
|
50
59
|
}
|
|
51
60
|
export declare const ResponseActions: FunctionComponent<ResponseActionProps>;
|
|
52
61
|
export default ResponseActions;
|
|
@@ -12,10 +12,12 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import { createElement as _createElement } from "react";
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { useEffect, useRef, useState } from 'react';
|
|
15
|
-
import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, OutlinedThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
|
|
15
|
+
import { ExternalLinkAltIcon, VolumeUpIcon, OutlinedThumbsUpIcon, ThumbsUpIcon, OutlinedThumbsDownIcon, ThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, PencilAltIcon } from '@patternfly/react-icons';
|
|
16
16
|
import ResponseActionButton from './ResponseActionButton';
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import { css } from '@patternfly/react-styles';
|
|
18
|
+
export const ResponseActions = (_a) => {
|
|
19
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
|
|
20
|
+
var { className, actions, persistActionSelection = false, useFilledIconsOnClick = false, showActionsOnInteraction = false } = _a, props = __rest(_a, ["className", "actions", "persistActionSelection", "useFilledIconsOnClick", "showActionsOnInteraction"]);
|
|
19
21
|
const [activeButton, setActiveButton] = useState();
|
|
20
22
|
const [clickStatePersisted, setClickStatePersisted] = useState(false);
|
|
21
23
|
const { positive, negative, copy, edit, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "edit", "share", "download", "listen"]);
|
|
@@ -63,6 +65,7 @@ export const ResponseActions = ({ actions, persistActionSelection = false }) =>
|
|
|
63
65
|
};
|
|
64
66
|
}, [clickStatePersisted, persistActionSelection]);
|
|
65
67
|
const handleClick = (e, id, onClick) => {
|
|
68
|
+
e.stopPropagation();
|
|
66
69
|
if (persistActionSelection) {
|
|
67
70
|
if (activeButton === id) {
|
|
68
71
|
// Toggle off if clicking the same button
|
|
@@ -80,9 +83,31 @@ export const ResponseActions = ({ actions, persistActionSelection = false }) =>
|
|
|
80
83
|
}
|
|
81
84
|
onClick && onClick(e);
|
|
82
85
|
};
|
|
83
|
-
|
|
86
|
+
const iconMap = {
|
|
87
|
+
positive: {
|
|
88
|
+
filled: _jsx(ThumbsUpIcon, {}),
|
|
89
|
+
outlined: _jsx(OutlinedThumbsUpIcon, {})
|
|
90
|
+
},
|
|
91
|
+
negative: {
|
|
92
|
+
filled: _jsx(ThumbsDownIcon, {}),
|
|
93
|
+
outlined: _jsx(OutlinedThumbsDownIcon, {})
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const getIcon = (actionName) => {
|
|
97
|
+
const isClicked = activeButton === actionName;
|
|
98
|
+
if (isClicked && useFilledIconsOnClick) {
|
|
99
|
+
return iconMap[actionName].filled;
|
|
100
|
+
}
|
|
101
|
+
return iconMap[actionName].outlined;
|
|
102
|
+
};
|
|
103
|
+
// We want to append the tooltip inline so that hovering the tooltip keeps the actions container visible
|
|
104
|
+
// when showActionsOnInteraction is true. Otherwise hovering the tooltip causes the actions container
|
|
105
|
+
// to disappear but the tooltip will remain visible.
|
|
106
|
+
const getTooltipContainer = () => responseActions.current || document.body;
|
|
107
|
+
const getTooltipProps = (tooltipProps) => (Object.assign(Object.assign({}, (showActionsOnInteraction && { appendTo: getTooltipContainer })), tooltipProps));
|
|
108
|
+
return (_jsxs("div", Object.assign({ ref: responseActions, className: css('pf-chatbot__response-actions', showActionsOnInteraction && 'pf-m-visible-interaction', className) }, props, { children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response', clickedAriaLabel: (_c = positive.ariaLabel) !== null && _c !== void 0 ? _c : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_d = positive.tooltipContent) !== null && _d !== void 0 ? _d : 'Good response', clickedTooltipContent: (_e = positive.clickedTooltipContent) !== null && _e !== void 0 ? _e : 'Good response recorded', tooltipProps: getTooltipProps(positive.tooltipProps), icon: getIcon('positive'), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response', clickedAriaLabel: (_g = negative.ariaLabel) !== null && _g !== void 0 ? _g : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_h = negative.tooltipContent) !== null && _h !== void 0 ? _h : 'Bad response', clickedTooltipContent: (_j = negative.clickedTooltipContent) !== null && _j !== void 0 ? _j : 'Bad response recorded', tooltipProps: getTooltipProps(negative.tooltipProps), icon: getIcon('negative'), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copy', clickedAriaLabel: (_l = copy.ariaLabel) !== null && _l !== void 0 ? _l : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_m = copy.tooltipContent) !== null && _m !== void 0 ? _m : 'Copy', clickedTooltipContent: (_o = copy.clickedTooltipContent) !== null && _o !== void 0 ? _o : 'Copied', tooltipProps: getTooltipProps(copy.tooltipProps), icon: _jsx(OutlinedCopyIcon, {}), isClicked: activeButton === 'copy', ref: copy.ref, "aria-expanded": copy['aria-expanded'], "aria-controls": copy['aria-controls'] }))), edit && (_jsx(ResponseActionButton, Object.assign({}, edit, { ariaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Edit', clickedAriaLabel: (_q = edit.ariaLabel) !== null && _q !== void 0 ? _q : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_r = edit.tooltipContent) !== null && _r !== void 0 ? _r : 'Edit ', clickedTooltipContent: (_s = edit.clickedTooltipContent) !== null && _s !== void 0 ? _s : 'Editing', tooltipProps: getTooltipProps(edit.tooltipProps), icon: _jsx(PencilAltIcon, {}), isClicked: activeButton === 'edit', ref: edit.ref, "aria-expanded": edit['aria-expanded'], "aria-controls": edit['aria-controls'] }))), share && (_jsx(ResponseActionButton, Object.assign({}, share, { ariaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Share', clickedAriaLabel: (_u = share.ariaLabel) !== null && _u !== void 0 ? _u : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_v = share.tooltipContent) !== null && _v !== void 0 ? _v : 'Share', clickedTooltipContent: (_w = share.clickedTooltipContent) !== null && _w !== void 0 ? _w : 'Shared', tooltipProps: getTooltipProps(share.tooltipProps), icon: _jsx(ExternalLinkAltIcon, {}), isClicked: activeButton === 'share', ref: share.ref, "aria-expanded": share['aria-expanded'], "aria-controls": share['aria-controls'] }))), download && (_jsx(ResponseActionButton, Object.assign({}, download, { ariaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Download', clickedAriaLabel: (_y = download.ariaLabel) !== null && _y !== void 0 ? _y : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_z = download.tooltipContent) !== null && _z !== void 0 ? _z : 'Download', clickedTooltipContent: (_0 = download.clickedTooltipContent) !== null && _0 !== void 0 ? _0 : 'Downloaded', tooltipProps: getTooltipProps(download.tooltipProps), icon: _jsx(DownloadIcon, {}), isClicked: activeButton === 'download', ref: download.ref, "aria-expanded": download['aria-expanded'], "aria-controls": download['aria-controls'] }))), listen && (_jsx(ResponseActionButton, Object.assign({}, listen, { ariaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listen', clickedAriaLabel: (_2 = listen.ariaLabel) !== null && _2 !== void 0 ? _2 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_3 = listen.tooltipContent) !== null && _3 !== void 0 ? _3 : 'Listen', clickedTooltipContent: (_4 = listen.clickedTooltipContent) !== null && _4 !== void 0 ? _4 : 'Listening', tooltipProps: getTooltipProps(listen.tooltipProps), icon: _jsx(VolumeUpIcon, {}), isClicked: activeButton === 'listen', ref: listen.ref, "aria-expanded": listen['aria-expanded'], "aria-controls": listen['aria-controls'] }))), Object.keys(additionalActions).map((action) => {
|
|
84
109
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
85
|
-
return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: (_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps, clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
|
|
86
|
-
})] }));
|
|
110
|
+
return (_createElement(ResponseActionButton, Object.assign({}, additionalActions[action], { key: action, ariaLabel: (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.ariaLabel, clickedAriaLabel: (_b = additionalActions[action]) === null || _b === void 0 ? void 0 : _b.clickedAriaLabel, onClick: (e) => { var _a; return handleClick(e, action, (_a = additionalActions[action]) === null || _a === void 0 ? void 0 : _a.onClick); }, className: (_c = additionalActions[action]) === null || _c === void 0 ? void 0 : _c.className, isDisabled: (_d = additionalActions[action]) === null || _d === void 0 ? void 0 : _d.isDisabled, tooltipContent: (_e = additionalActions[action]) === null || _e === void 0 ? void 0 : _e.tooltipContent, tooltipProps: getTooltipProps((_f = additionalActions[action]) === null || _f === void 0 ? void 0 : _f.tooltipProps), clickedTooltipContent: (_g = additionalActions[action]) === null || _g === void 0 ? void 0 : _g.clickedTooltipContent, icon: (_h = additionalActions[action]) === null || _h === void 0 ? void 0 : _h.icon, isClicked: activeButton === action, ref: (_j = additionalActions[action]) === null || _j === void 0 ? void 0 : _j.ref, "aria-expanded": (_k = additionalActions[action]) === null || _k === void 0 ? void 0 : _k['aria-expanded'], "aria-controls": (_l = additionalActions[action]) === null || _l === void 0 ? void 0 : _l['aria-controls'] })));
|
|
111
|
+
})] })));
|
|
87
112
|
};
|
|
88
113
|
export default ResponseActions;
|
|
@@ -7,13 +7,27 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { render, screen } from '@testing-library/react';
|
|
12
12
|
import '@testing-library/jest-dom';
|
|
13
13
|
import ResponseActions from './ResponseActions';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
|
16
16
|
import Message from '../Message';
|
|
17
|
+
// Mock the icon components
|
|
18
|
+
jest.mock('@patternfly/react-icons', () => ({
|
|
19
|
+
OutlinedThumbsUpIcon: () => _jsx("div", { children: "OutlinedThumbsUpIcon" }),
|
|
20
|
+
ThumbsUpIcon: () => _jsx("div", { children: "ThumbsUpIcon" }),
|
|
21
|
+
OutlinedThumbsDownIcon: () => _jsx("div", { children: "OutlinedThumbsDownIcon" }),
|
|
22
|
+
ThumbsDownIcon: () => _jsx("div", { children: "ThumbsDownIcon" }),
|
|
23
|
+
OutlinedCopyIcon: () => _jsx("div", { children: "OutlinedCopyIcon" }),
|
|
24
|
+
DownloadIcon: () => _jsx("div", { children: "DownloadIcon" }),
|
|
25
|
+
InfoCircleIcon: () => _jsx("div", { children: "InfoCircleIcon" }),
|
|
26
|
+
RedoIcon: () => _jsx("div", { children: "RedoIcon" }),
|
|
27
|
+
ExternalLinkAltIcon: () => _jsx("div", { children: "ExternalLinkAltIcon" }),
|
|
28
|
+
VolumeUpIcon: () => _jsx("div", { children: "VolumeUpIcon" }),
|
|
29
|
+
PencilAltIcon: () => _jsx("div", { children: "PencilAltIcon" })
|
|
30
|
+
}));
|
|
17
31
|
const ALL_ACTIONS = [
|
|
18
32
|
{ type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
|
|
19
33
|
{ type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
|
|
@@ -310,4 +324,124 @@ describe('ResponseActions', () => {
|
|
|
310
324
|
yield userEvent.click(customBtn);
|
|
311
325
|
expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
|
312
326
|
}));
|
|
327
|
+
it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
|
|
328
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
329
|
+
positive: { onClick: jest.fn() },
|
|
330
|
+
negative: { onClick: jest.fn() }
|
|
331
|
+
}, showActionsOnInteraction: true }));
|
|
332
|
+
expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
|
|
333
|
+
});
|
|
334
|
+
it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
|
|
335
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
336
|
+
positive: { onClick: jest.fn() },
|
|
337
|
+
negative: { onClick: jest.fn() }
|
|
338
|
+
}, showActionsOnInteraction: false }));
|
|
339
|
+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
|
|
340
|
+
});
|
|
341
|
+
it('should not apply pf-m-visible-interaction class by default', () => {
|
|
342
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
343
|
+
positive: { onClick: jest.fn() },
|
|
344
|
+
negative: { onClick: jest.fn() }
|
|
345
|
+
} }));
|
|
346
|
+
expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
|
|
347
|
+
});
|
|
348
|
+
it('should render with custom className', () => {
|
|
349
|
+
render(_jsx(ResponseActions, { "data-testid": "test-id", actions: {
|
|
350
|
+
positive: { onClick: jest.fn() },
|
|
351
|
+
negative: { onClick: jest.fn() }
|
|
352
|
+
}, className: "custom-class" }));
|
|
353
|
+
expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
|
|
354
|
+
});
|
|
355
|
+
describe('icon swapping with useFilledIconsOnClick', () => {
|
|
356
|
+
it('should render outline icons by default', () => {
|
|
357
|
+
render(_jsx(ResponseActions, { actions: {
|
|
358
|
+
positive: { onClick: jest.fn() },
|
|
359
|
+
negative: { onClick: jest.fn() }
|
|
360
|
+
} }));
|
|
361
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
362
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
363
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
364
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
describe('positive actions', () => {
|
|
367
|
+
it('should not swap positive icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
368
|
+
const user = userEvent.setup();
|
|
369
|
+
render(_jsx(ResponseActions, { actions: {
|
|
370
|
+
positive: { onClick: jest.fn() }
|
|
371
|
+
}, useFilledIconsOnClick: false }));
|
|
372
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
373
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
374
|
+
expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
|
|
375
|
+
}));
|
|
376
|
+
it('should swap positive icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
377
|
+
const user = userEvent.setup();
|
|
378
|
+
render(_jsx(ResponseActions, { actions: {
|
|
379
|
+
positive: { onClick: jest.fn() }
|
|
380
|
+
}, useFilledIconsOnClick: true }));
|
|
381
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
382
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
383
|
+
expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
|
|
384
|
+
}));
|
|
385
|
+
it('should revert positive icon to outline icon when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
386
|
+
const user = userEvent.setup();
|
|
387
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
388
|
+
positive: { onClick: jest.fn() }
|
|
389
|
+
}, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
390
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
391
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
392
|
+
yield user.click(screen.getByTestId('outside'));
|
|
393
|
+
expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
|
|
394
|
+
}));
|
|
395
|
+
it('should not revert positive icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
396
|
+
const user = userEvent.setup();
|
|
397
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
398
|
+
positive: { onClick: jest.fn() }
|
|
399
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
400
|
+
yield user.click(screen.getByRole('button', { name: 'Good response' }));
|
|
401
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
402
|
+
yield user.click(screen.getByTestId('outside'));
|
|
403
|
+
expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
|
|
404
|
+
}));
|
|
405
|
+
describe('negative actions', () => {
|
|
406
|
+
it('should not swap negative icon when clicked and useFilledIconsOnClick is false', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
407
|
+
const user = userEvent.setup();
|
|
408
|
+
render(_jsx(ResponseActions, { actions: {
|
|
409
|
+
negative: { onClick: jest.fn() }
|
|
410
|
+
}, useFilledIconsOnClick: false }));
|
|
411
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
412
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
413
|
+
expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
|
|
414
|
+
}));
|
|
415
|
+
it('should swap negative icon from outline to filled when clicked with useFilledIconsOnClick', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
416
|
+
const user = userEvent.setup();
|
|
417
|
+
render(_jsx(ResponseActions, { actions: {
|
|
418
|
+
negative: { onClick: jest.fn() }
|
|
419
|
+
}, useFilledIconsOnClick: true }));
|
|
420
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
421
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
422
|
+
expect(screen.queryByText('OutlinedThumbsDownIcon')).not.toBeInTheDocument();
|
|
423
|
+
}));
|
|
424
|
+
it('should revert negative icon to outline when clicking outside', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
425
|
+
const user = userEvent.setup();
|
|
426
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
427
|
+
negative: { onClick: jest.fn() }
|
|
428
|
+
}, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
429
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
430
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
431
|
+
yield user.click(screen.getByTestId('outside'));
|
|
432
|
+
expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
|
|
433
|
+
}));
|
|
434
|
+
it('should not revert negative icon to outline icon when clicking outside if persistActionSelection is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
435
|
+
const user = userEvent.setup();
|
|
436
|
+
render(_jsxs("div", { children: [_jsx(ResponseActions, { actions: {
|
|
437
|
+
negative: { onClick: jest.fn() }
|
|
438
|
+
}, persistActionSelection: true, useFilledIconsOnClick: true }), _jsx("div", { "data-testid": "outside", children: "Outside" })] }));
|
|
439
|
+
yield user.click(screen.getByRole('button', { name: 'Bad response' }));
|
|
440
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
441
|
+
yield user.click(screen.getByTestId('outside'));
|
|
442
|
+
expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
|
|
443
|
+
}));
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
});
|
|
313
447
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/chatbot",
|
|
3
|
-
"version": "6.6.0-prerelease.
|
|
3
|
+
"version": "6.6.0-prerelease.6",
|
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithIconSwapping.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FunctionComponent } from 'react';
|
|
2
|
+
|
|
3
|
+
import Message from '@patternfly/chatbot/dist/dynamic/Message';
|
|
4
|
+
import patternflyAvatar from './patternfly_avatar.jpg';
|
|
5
|
+
|
|
6
|
+
export const IconSwappingExample: FunctionComponent = () => (
|
|
7
|
+
<Message
|
|
8
|
+
name="Bot"
|
|
9
|
+
role="bot"
|
|
10
|
+
avatar={patternflyAvatar}
|
|
11
|
+
content="Click the response actions to see the outlined icons swapped with the filled variants!"
|
|
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('Copied') }
|
|
19
|
+
}}
|
|
20
|
+
useFilledIconsOnClick
|
|
21
|
+
/>
|
|
22
|
+
);
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithResponseActions.tsx
CHANGED
|
@@ -4,22 +4,43 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
|
|
|
4
4
|
import patternflyAvatar from './patternfly_avatar.jpg';
|
|
5
5
|
|
|
6
6
|
export const ResponseActionExample: FunctionComponent = () => (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
</>
|
|
25
46
|
);
|
|
@@ -95,14 +95,16 @@ For example, you can use the default divider to display a "timestamp" for more s
|
|
|
95
95
|
|
|
96
96
|
### Message actions
|
|
97
97
|
|
|
98
|
-
|
|
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:
|
|
99
99
|
|
|
100
|
-
-
|
|
101
|
-
- Copy
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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.
|
|
104
104
|
|
|
105
|
-
|
|
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.
|
|
106
108
|
|
|
107
109
|
```js file="./MessageWithResponseActions.tsx"
|
|
108
110
|
|
|
@@ -138,6 +140,16 @@ When `persistActionSelection` is `true`:
|
|
|
138
140
|
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
### Message actions that fill
|
|
144
|
+
|
|
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.
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
```js file="./MessageWithIconSwapping.tsx"
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
141
153
|
### Multiple messsage action groups
|
|
142
154
|
|
|
143
155
|
To maintain finer control over message action selection behavior, you can create groups of actions by passing an array of objects to the `actions` prop. This allows you to separate actions into conceptually or functionally different groups and implement different behavior for each group as needed. For example, you could separate feedback actions (thumbs up/down) form utility actions (copy and download), and have different selection behaviors for each group.
|
package/src/Message/Message.scss
CHANGED
|
@@ -92,6 +92,18 @@
|
|
|
92
92
|
gap: var(--pf-t--global--spacer--sm);
|
|
93
93
|
}
|
|
94
94
|
|
|
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
|
+
|
|
95
107
|
// targets footnotes specifically
|
|
96
108
|
.footnotes,
|
|
97
109
|
.pf-chatbot__message-text.footnotes {
|