@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.
@@ -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
- export const ResponseActions = ({ actions, persistActionSelection = false }) => {
18
- var _a, _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;
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
- return (_jsxs("div", { ref: responseActions, className: "pf-chatbot__response-actions", children: [positive && (_jsx(ResponseActionButton, Object.assign({}, positive, { ariaLabel: (_a = positive.ariaLabel) !== null && _a !== void 0 ? _a : 'Good response', clickedAriaLabel: (_b = positive.ariaLabel) !== null && _b !== void 0 ? _b : 'Good response recorded', onClick: (e) => handleClick(e, 'positive', positive.onClick), className: positive.className, isDisabled: positive.isDisabled, tooltipContent: (_c = positive.tooltipContent) !== null && _c !== void 0 ? _c : 'Good response', clickedTooltipContent: (_d = positive.clickedTooltipContent) !== null && _d !== void 0 ? _d : 'Good response recorded', tooltipProps: positive.tooltipProps, icon: _jsx(OutlinedThumbsUpIcon, {}), isClicked: activeButton === 'positive', ref: positive.ref, "aria-expanded": positive['aria-expanded'], "aria-controls": positive['aria-controls'] }))), negative && (_jsx(ResponseActionButton, Object.assign({}, negative, { ariaLabel: (_e = negative.ariaLabel) !== null && _e !== void 0 ? _e : 'Bad response', clickedAriaLabel: (_f = negative.ariaLabel) !== null && _f !== void 0 ? _f : 'Bad response recorded', onClick: (e) => handleClick(e, 'negative', negative.onClick), className: negative.className, isDisabled: negative.isDisabled, tooltipContent: (_g = negative.tooltipContent) !== null && _g !== void 0 ? _g : 'Bad response', clickedTooltipContent: (_h = negative.clickedTooltipContent) !== null && _h !== void 0 ? _h : 'Bad response recorded', tooltipProps: negative.tooltipProps, icon: _jsx(OutlinedThumbsDownIcon, {}), isClicked: activeButton === 'negative', ref: negative.ref, "aria-expanded": negative['aria-expanded'], "aria-controls": negative['aria-controls'] }))), copy && (_jsx(ResponseActionButton, Object.assign({}, copy, { ariaLabel: (_j = copy.ariaLabel) !== null && _j !== void 0 ? _j : 'Copy', clickedAriaLabel: (_k = copy.ariaLabel) !== null && _k !== void 0 ? _k : 'Copied', onClick: (e) => handleClick(e, 'copy', copy.onClick), className: copy.className, isDisabled: copy.isDisabled, tooltipContent: (_l = copy.tooltipContent) !== null && _l !== void 0 ? _l : 'Copy', clickedTooltipContent: (_m = copy.clickedTooltipContent) !== null && _m !== void 0 ? _m : 'Copied', tooltipProps: 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: (_o = edit.ariaLabel) !== null && _o !== void 0 ? _o : 'Edit', clickedAriaLabel: (_p = edit.ariaLabel) !== null && _p !== void 0 ? _p : 'Editing', onClick: (e) => handleClick(e, 'edit', edit.onClick), className: edit.className, isDisabled: edit.isDisabled, tooltipContent: (_q = edit.tooltipContent) !== null && _q !== void 0 ? _q : 'Edit ', clickedTooltipContent: (_r = edit.clickedTooltipContent) !== null && _r !== void 0 ? _r : 'Editing', tooltipProps: 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: (_s = share.ariaLabel) !== null && _s !== void 0 ? _s : 'Share', clickedAriaLabel: (_t = share.ariaLabel) !== null && _t !== void 0 ? _t : 'Shared', onClick: (e) => handleClick(e, 'share', share.onClick), className: share.className, isDisabled: share.isDisabled, tooltipContent: (_u = share.tooltipContent) !== null && _u !== void 0 ? _u : 'Share', clickedTooltipContent: (_v = share.clickedTooltipContent) !== null && _v !== void 0 ? _v : 'Shared', tooltipProps: 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: (_w = download.ariaLabel) !== null && _w !== void 0 ? _w : 'Download', clickedAriaLabel: (_x = download.ariaLabel) !== null && _x !== void 0 ? _x : 'Downloaded', onClick: (e) => handleClick(e, 'download', download.onClick), className: download.className, isDisabled: download.isDisabled, tooltipContent: (_y = download.tooltipContent) !== null && _y !== void 0 ? _y : 'Download', clickedTooltipContent: (_z = download.clickedTooltipContent) !== null && _z !== void 0 ? _z : 'Downloaded', tooltipProps: 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: (_0 = listen.ariaLabel) !== null && _0 !== void 0 ? _0 : 'Listen', clickedAriaLabel: (_1 = listen.ariaLabel) !== null && _1 !== void 0 ? _1 : 'Listening', onClick: (e) => handleClick(e, 'listen', listen.onClick), className: listen.className, isDisabled: listen.isDisabled, tooltipContent: (_2 = listen.tooltipContent) !== null && _2 !== void 0 ? _2 : 'Listen', clickedTooltipContent: (_3 = listen.clickedTooltipContent) !== null && _3 !== void 0 ? _3 : 'Listening', tooltipProps: 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) => {
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.4",
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",
@@ -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
+ );
@@ -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
- <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
+ </>
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
- 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:
99
99
 
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.
104
104
 
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.
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.
@@ -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 {