@patternfly/chatbot 2.2.0-prerelease.3 → 2.2.0-prerelease.5
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/ResponseActions/ResponseActionButton.d.ts +6 -0
- package/dist/cjs/ResponseActions/ResponseActionButton.js +10 -2
- package/dist/cjs/ResponseActions/ResponseActionButton.test.d.ts +1 -0
- package/dist/cjs/ResponseActions/ResponseActionButton.test.js +54 -0
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +4 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +26 -9
- package/dist/cjs/ResponseActions/ResponseActions.test.js +79 -5
- package/dist/cjs/TermsOfUse/TermsOfUse.d.ts +34 -0
- package/dist/cjs/TermsOfUse/TermsOfUse.js +49 -0
- package/dist/cjs/TermsOfUse/TermsOfUse.test.d.ts +1 -0
- package/dist/cjs/TermsOfUse/TermsOfUse.test.js +79 -0
- package/dist/cjs/TermsOfUse/index.d.ts +2 -0
- package/dist/cjs/TermsOfUse/index.js +23 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/css/main.css +66 -4
- package/dist/css/main.css.map +1 -1
- package/dist/dynamic/TermsOfUse/package.json +1 -0
- package/dist/esm/ResponseActions/ResponseActionButton.d.ts +6 -0
- package/dist/esm/ResponseActions/ResponseActionButton.js +10 -2
- package/dist/esm/ResponseActions/ResponseActionButton.test.d.ts +1 -0
- package/dist/esm/ResponseActions/ResponseActionButton.test.js +49 -0
- package/dist/esm/ResponseActions/ResponseActions.d.ts +4 -0
- package/dist/esm/ResponseActions/ResponseActions.js +26 -9
- package/dist/esm/ResponseActions/ResponseActions.test.js +79 -5
- package/dist/esm/TermsOfUse/TermsOfUse.d.ts +34 -0
- package/dist/esm/TermsOfUse/TermsOfUse.js +42 -0
- package/dist/esm/TermsOfUse/TermsOfUse.test.d.ts +1 -0
- package/dist/esm/TermsOfUse/TermsOfUse.test.js +74 -0
- package/dist/esm/TermsOfUse/index.d.ts +2 -0
- package/dist/esm/TermsOfUse/index.js +2 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +4 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +13 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/PF-TermsAndConditionsHeader.svg +148 -0
- package/patternfly-docs/content/extensions/chatbot/examples/UI/TermsOfUse.tsx +147 -0
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +14 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +2 -2
- package/src/ResponseActions/ResponseActionButton.test.tsx +52 -0
- package/src/ResponseActions/ResponseActionButton.tsx +46 -27
- package/src/ResponseActions/ResponseActions.scss +10 -8
- package/src/ResponseActions/ResponseActions.test.tsx +103 -5
- package/src/ResponseActions/ResponseActions.tsx +54 -7
- package/src/TermsOfUse/TermsOfUse.scss +66 -0
- package/src/TermsOfUse/TermsOfUse.test.tsx +138 -0
- package/src/TermsOfUse/TermsOfUse.tsx +117 -0
- package/src/TermsOfUse/index.ts +3 -0
- package/src/index.ts +3 -0
- package/src/main.scss +1 -0
@@ -4,27 +4,32 @@ import '@testing-library/jest-dom';
|
|
4
4
|
import ResponseActions from './ResponseActions';
|
5
5
|
import userEvent from '@testing-library/user-event';
|
6
6
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
7
|
+
import Message from '../Message';
|
7
8
|
|
8
9
|
const ALL_ACTIONS = [
|
9
|
-
{ type: 'positive', label: 'Good response' },
|
10
|
-
{ type: 'negative', label: 'Bad response' },
|
11
|
-
{ type: 'copy', label: 'Copy' },
|
12
|
-
{ type: 'share', label: 'Share' },
|
13
|
-
{ type: 'listen', label: 'Listen' }
|
10
|
+
{ type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
|
11
|
+
{ type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
|
12
|
+
{ type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
|
13
|
+
{ type: 'share', label: 'Share', clickedLabel: 'Shared' },
|
14
|
+
{ type: 'listen', label: 'Listen', clickedLabel: 'Listening' }
|
14
15
|
];
|
15
16
|
|
16
17
|
const CUSTOM_ACTIONS = [
|
17
18
|
{
|
18
19
|
regenerate: {
|
19
20
|
ariaLabel: 'Regenerate',
|
21
|
+
clickedAriaLabel: 'Regenerated',
|
20
22
|
onClick: jest.fn(),
|
21
23
|
tooltipContent: 'Regenerate',
|
24
|
+
clickedTooltipContent: 'Regenerated',
|
22
25
|
icon: <RedoIcon />
|
23
26
|
},
|
24
27
|
download: {
|
25
28
|
ariaLabel: 'Download',
|
29
|
+
clickedAriaLabel: 'Downloaded',
|
26
30
|
onClick: jest.fn(),
|
27
31
|
tooltipContent: 'Download',
|
32
|
+
clickedTooltipContent: 'Downloaded',
|
28
33
|
icon: <DownloadIcon />
|
29
34
|
},
|
30
35
|
info: {
|
@@ -37,6 +42,81 @@ const CUSTOM_ACTIONS = [
|
|
37
42
|
];
|
38
43
|
|
39
44
|
describe('ResponseActions', () => {
|
45
|
+
afterEach(() => {
|
46
|
+
jest.clearAllMocks();
|
47
|
+
});
|
48
|
+
it('should handle click within group of buttons correctly', async () => {
|
49
|
+
render(
|
50
|
+
<ResponseActions
|
51
|
+
actions={{
|
52
|
+
positive: { onClick: jest.fn() },
|
53
|
+
negative: { onClick: jest.fn() },
|
54
|
+
copy: { onClick: jest.fn() },
|
55
|
+
share: { onClick: jest.fn() },
|
56
|
+
listen: { onClick: jest.fn() }
|
57
|
+
}}
|
58
|
+
/>
|
59
|
+
);
|
60
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
61
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
62
|
+
const copyBtn = screen.getByRole('button', { name: 'Copy' });
|
63
|
+
const shareBtn = screen.getByRole('button', { name: 'Share' });
|
64
|
+
const listenBtn = screen.getByRole('button', { name: 'Listen' });
|
65
|
+
const buttons = [goodBtn, badBtn, copyBtn, shareBtn, listenBtn];
|
66
|
+
buttons.forEach((button) => {
|
67
|
+
expect(button).toBeTruthy();
|
68
|
+
});
|
69
|
+
await userEvent.click(goodBtn);
|
70
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
71
|
+
'pf-chatbot__button--response-action-clicked'
|
72
|
+
);
|
73
|
+
let unclickedButtons = buttons.filter((button) => button !== goodBtn);
|
74
|
+
unclickedButtons.forEach((button) => {
|
75
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
76
|
+
});
|
77
|
+
await userEvent.click(badBtn);
|
78
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
79
|
+
'pf-chatbot__button--response-action-clicked'
|
80
|
+
);
|
81
|
+
unclickedButtons = buttons.filter((button) => button !== badBtn);
|
82
|
+
unclickedButtons.forEach((button) => {
|
83
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
84
|
+
});
|
85
|
+
});
|
86
|
+
it('should handle click outside of group of buttons correctly', async () => {
|
87
|
+
// using message just so we have something outside the group that's rendered
|
88
|
+
render(
|
89
|
+
<Message
|
90
|
+
name="Bot"
|
91
|
+
role="bot"
|
92
|
+
avatar=""
|
93
|
+
content="Example with all prebuilt actions"
|
94
|
+
actions={{
|
95
|
+
positive: {},
|
96
|
+
negative: {}
|
97
|
+
}}
|
98
|
+
/>
|
99
|
+
);
|
100
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
101
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
102
|
+
expect(goodBtn).toBeTruthy();
|
103
|
+
expect(badBtn).toBeTruthy();
|
104
|
+
|
105
|
+
await userEvent.click(goodBtn);
|
106
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
107
|
+
'pf-chatbot__button--response-action-clicked'
|
108
|
+
);
|
109
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
110
|
+
|
111
|
+
await userEvent.click(badBtn);
|
112
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
113
|
+
'pf-chatbot__button--response-action-clicked'
|
114
|
+
);
|
115
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
116
|
+
await userEvent.click(screen.getByText('Example with all prebuilt actions'));
|
117
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
118
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
119
|
+
});
|
40
120
|
it('should render buttons correctly', () => {
|
41
121
|
ALL_ACTIONS.forEach(({ type, label }) => {
|
42
122
|
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
@@ -53,6 +133,24 @@ describe('ResponseActions', () => {
|
|
53
133
|
});
|
54
134
|
});
|
55
135
|
|
136
|
+
it('should swap clicked and non-clicked aria labels on click', async () => {
|
137
|
+
ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
|
138
|
+
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
139
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
140
|
+
await userEvent.click(screen.getByRole('button', { name: label }));
|
141
|
+
expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
|
142
|
+
});
|
143
|
+
});
|
144
|
+
|
145
|
+
it('should swap clicked and non-clicked tooltips on click', async () => {
|
146
|
+
ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
|
147
|
+
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
148
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
149
|
+
await userEvent.click(screen.getByRole('button', { name: label }));
|
150
|
+
expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
|
151
|
+
});
|
152
|
+
});
|
153
|
+
|
56
154
|
it('should be able to change aria labels', () => {
|
57
155
|
const actions = [
|
58
156
|
{ type: 'positive', ariaLabel: 'Thumbs up' },
|
@@ -12,6 +12,8 @@ import { TooltipProps } from '@patternfly/react-core';
|
|
12
12
|
export interface ActionProps {
|
13
13
|
/** Aria-label for the button */
|
14
14
|
ariaLabel?: string;
|
15
|
+
/** Aria-label for the button, shown when the button is clicked. */
|
16
|
+
clickedAriaLabel?: string;
|
15
17
|
/** On-click handler for the button */
|
16
18
|
onClick?: ((event: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent) => void) | undefined;
|
17
19
|
/** Class name for the button */
|
@@ -20,6 +22,8 @@ export interface ActionProps {
|
|
20
22
|
isDisabled?: boolean;
|
21
23
|
/** Content shown in the tooltip */
|
22
24
|
tooltipContent?: string;
|
25
|
+
/** Content shown in the tooltip when the button is clicked. */
|
26
|
+
clickedTooltipContent?: string;
|
23
27
|
/** Props to control the PF Tooltip component */
|
24
28
|
tooltipProps?: TooltipProps;
|
25
29
|
/** Icon for custom response action */
|
@@ -38,74 +42,117 @@ export interface ResponseActionProps {
|
|
38
42
|
}
|
39
43
|
|
40
44
|
export const ResponseActions: React.FunctionComponent<ResponseActionProps> = ({ actions }) => {
|
45
|
+
const [activeButton, setActiveButton] = React.useState<string>();
|
41
46
|
const { positive, negative, copy, share, listen, ...additionalActions } = actions;
|
47
|
+
const responseActions = React.useRef<HTMLDivElement>(null);
|
48
|
+
|
49
|
+
React.useEffect(() => {
|
50
|
+
const handleClickOutside = (e) => {
|
51
|
+
if (responseActions.current && !responseActions.current.contains(e.target)) {
|
52
|
+
setActiveButton(undefined);
|
53
|
+
}
|
54
|
+
};
|
55
|
+
window.addEventListener('click', handleClickOutside);
|
56
|
+
|
57
|
+
return () => {
|
58
|
+
window.removeEventListener('click', handleClickOutside);
|
59
|
+
};
|
60
|
+
}, []);
|
61
|
+
|
62
|
+
const handleClick = (
|
63
|
+
e: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent,
|
64
|
+
id: string,
|
65
|
+
onClick?: (event: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
|
66
|
+
) => {
|
67
|
+
setActiveButton(id);
|
68
|
+
onClick && onClick(e);
|
69
|
+
};
|
70
|
+
|
42
71
|
return (
|
43
|
-
<div className="pf-chatbot__response-actions">
|
72
|
+
<div ref={responseActions} className="pf-chatbot__response-actions">
|
44
73
|
{positive && (
|
45
74
|
<ResponseActionButton
|
46
75
|
ariaLabel={positive.ariaLabel ?? 'Good response'}
|
47
|
-
|
76
|
+
clickedAriaLabel={positive.ariaLabel ?? 'Response recorded'}
|
77
|
+
onClick={(e) => handleClick(e, 'positive', positive.onClick)}
|
48
78
|
className={positive.className}
|
49
79
|
isDisabled={positive.isDisabled}
|
50
80
|
tooltipContent={positive.tooltipContent ?? 'Good response'}
|
81
|
+
clickedTooltipContent={positive.clickedTooltipContent ?? 'Response recorded'}
|
51
82
|
tooltipProps={positive.tooltipProps}
|
52
83
|
icon={<OutlinedThumbsUpIcon />}
|
84
|
+
isClicked={activeButton === 'positive'}
|
53
85
|
></ResponseActionButton>
|
54
86
|
)}
|
55
87
|
{negative && (
|
56
88
|
<ResponseActionButton
|
57
89
|
ariaLabel={negative.ariaLabel ?? 'Bad response'}
|
58
|
-
|
90
|
+
clickedAriaLabel={negative.ariaLabel ?? 'Response recorded'}
|
91
|
+
onClick={(e) => handleClick(e, 'negative', negative.onClick)}
|
59
92
|
className={negative.className}
|
60
93
|
isDisabled={negative.isDisabled}
|
61
94
|
tooltipContent={negative.tooltipContent ?? 'Bad response'}
|
95
|
+
clickedTooltipContent={negative.clickedTooltipContent ?? 'Response recorded'}
|
62
96
|
tooltipProps={negative.tooltipProps}
|
63
97
|
icon={<OutlinedThumbsDownIcon />}
|
98
|
+
isClicked={activeButton === 'negative'}
|
64
99
|
></ResponseActionButton>
|
65
100
|
)}
|
66
101
|
{copy && (
|
67
102
|
<ResponseActionButton
|
68
103
|
ariaLabel={copy.ariaLabel ?? 'Copy'}
|
69
|
-
|
104
|
+
clickedAriaLabel={copy.ariaLabel ?? 'Copied'}
|
105
|
+
onClick={(e) => handleClick(e, 'copy', copy.onClick)}
|
70
106
|
className={copy.className}
|
71
107
|
isDisabled={copy.isDisabled}
|
72
108
|
tooltipContent={copy.tooltipContent ?? 'Copy'}
|
109
|
+
clickedTooltipContent={copy.clickedTooltipContent ?? 'Copied'}
|
73
110
|
tooltipProps={copy.tooltipProps}
|
74
111
|
icon={<OutlinedCopyIcon />}
|
112
|
+
isClicked={activeButton === 'copy'}
|
75
113
|
></ResponseActionButton>
|
76
114
|
)}
|
77
115
|
{share && (
|
78
116
|
<ResponseActionButton
|
79
117
|
ariaLabel={share.ariaLabel ?? 'Share'}
|
80
|
-
|
118
|
+
clickedAriaLabel={share.ariaLabel ?? 'Shared'}
|
119
|
+
onClick={(e) => handleClick(e, 'share', share.onClick)}
|
81
120
|
className={share.className}
|
82
121
|
isDisabled={share.isDisabled}
|
83
122
|
tooltipContent={share.tooltipContent ?? 'Share'}
|
123
|
+
clickedTooltipContent={share.clickedTooltipContent ?? 'Shared'}
|
84
124
|
tooltipProps={share.tooltipProps}
|
85
125
|
icon={<ExternalLinkAltIcon />}
|
126
|
+
isClicked={activeButton === 'share'}
|
86
127
|
></ResponseActionButton>
|
87
128
|
)}
|
88
129
|
{listen && (
|
89
130
|
<ResponseActionButton
|
90
131
|
ariaLabel={listen.ariaLabel ?? 'Listen'}
|
91
|
-
|
132
|
+
clickedAriaLabel={listen.ariaLabel ?? 'Listening'}
|
133
|
+
onClick={(e) => handleClick(e, 'listen', listen.onClick)}
|
92
134
|
className={listen.className}
|
93
135
|
isDisabled={listen.isDisabled}
|
94
136
|
tooltipContent={listen.tooltipContent ?? 'Listen'}
|
137
|
+
clickedTooltipContent={listen.clickedTooltipContent ?? 'Listening'}
|
95
138
|
tooltipProps={listen.tooltipProps}
|
96
139
|
icon={<VolumeUpIcon />}
|
140
|
+
isClicked={activeButton === 'listen'}
|
97
141
|
></ResponseActionButton>
|
98
142
|
)}
|
99
143
|
{Object.keys(additionalActions).map((action) => (
|
100
144
|
<ResponseActionButton
|
101
145
|
key={action}
|
102
146
|
ariaLabel={additionalActions[action]?.ariaLabel}
|
103
|
-
|
147
|
+
clickedAriaLabel={additionalActions[action]?.clickedAriaLabel}
|
148
|
+
onClick={(e) => handleClick(e, action, additionalActions[action]?.onClick)}
|
104
149
|
className={additionalActions[action]?.className}
|
105
150
|
isDisabled={additionalActions[action]?.isDisabled}
|
106
151
|
tooltipContent={additionalActions[action]?.tooltipContent}
|
107
152
|
tooltipProps={additionalActions[action]?.tooltipProps}
|
153
|
+
clickedTooltipContent={additionalActions[action]?.clickedTooltipContent}
|
108
154
|
icon={additionalActions[action]?.icon}
|
155
|
+
isClicked={activeButton === action}
|
109
156
|
/>
|
110
157
|
))}
|
111
158
|
</div>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
.pf-chatbot__terms-of-use-modal {
|
2
|
+
.pf-v6-c-content {
|
3
|
+
font-size: var(--pf-t--global--font--size--body--lg);
|
4
|
+
|
5
|
+
h2 {
|
6
|
+
font-size: var(--pf-t--global--icon--size--font--heading--h2);
|
7
|
+
font-family: var(--pf-t--global--font--family--heading);
|
8
|
+
margin-bottom: var(--pf-t--global--spacer--md);
|
9
|
+
margin-top: var(--pf-t--global--spacer--md);
|
10
|
+
font-weight: var(--pf-t--global--font--weight--heading--default);
|
11
|
+
}
|
12
|
+
h2:first-of-type {
|
13
|
+
margin-top: 0;
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
.pf-chatbot__terms-of-use--header {
|
18
|
+
display: flex;
|
19
|
+
align-items: center;
|
20
|
+
justify-content: center;
|
21
|
+
flex-direction: column;
|
22
|
+
gap: var(--pf-t--global--spacer--xl);
|
23
|
+
margin-block-start: var(--pf-t--global--spacer--xl);
|
24
|
+
}
|
25
|
+
|
26
|
+
.pf-chatbot__terms-of-use--title {
|
27
|
+
font-size: var(--pf-t--global--font--size--heading--h1);
|
28
|
+
font-family: var(--pf-t--global--font--family--heading);
|
29
|
+
font-weight: var(--pf-t--global--font--weight--heading--bold);
|
30
|
+
}
|
31
|
+
|
32
|
+
.pf-chatbot__terms-of-use--footer {
|
33
|
+
margin-block-start: var(--pf-t--global--spacer--md);
|
34
|
+
}
|
35
|
+
|
36
|
+
.pf-chatbot__terms-of-use--section {
|
37
|
+
display: flex;
|
38
|
+
flex-direction: column;
|
39
|
+
height: 100%;
|
40
|
+
width: 100%;
|
41
|
+
}
|
42
|
+
|
43
|
+
// for handling zoom conditions; zoom to 125% or higher to see this
|
44
|
+
@media screen and (max-height: 620px) {
|
45
|
+
.pf-v6-c-modal-box__body {
|
46
|
+
--pf-v6-c-modal-box__body--MinHeight: auto;
|
47
|
+
overflow: visible;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
.pf-chatbot__chatbot-modal.pf-chatbot__chatbot-modal--fullscreen.pf-chatbot__terms-of-use-modal.pf-chatbot__terms-of-use-modal--fullscreen,
|
53
|
+
.pf-chatbot__chatbot-modal.pf-chatbot__chatbot-modal--embedded.pf-chatbot__terms-of-use-modal.pf-chatbot__terms-of-use-modal--embedded {
|
54
|
+
// override parent modal style
|
55
|
+
height: inherit;
|
56
|
+
|
57
|
+
.pf-v6-c-content {
|
58
|
+
h2 {
|
59
|
+
font-size: var(--pf-t--global--icon--size--font--heading--h1);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
.pf-chatbot__terms-of-use--title {
|
64
|
+
font-size: var(--pf-t--global--font--size--heading--2xl);
|
65
|
+
}
|
66
|
+
}
|
@@ -0,0 +1,138 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
5
|
+
import TermsOfUse from './TermsOfUse';
|
6
|
+
import { Content } from '@patternfly/react-core';
|
7
|
+
|
8
|
+
const handleModalToggle = jest.fn();
|
9
|
+
const onPrimaryAction = jest.fn();
|
10
|
+
const onSecondaryAction = jest.fn();
|
11
|
+
|
12
|
+
const body = (
|
13
|
+
<Content>
|
14
|
+
<h1>Heading 1</h1>
|
15
|
+
<p>Legal text</p>
|
16
|
+
</Content>
|
17
|
+
);
|
18
|
+
describe('TermsOfUse', () => {
|
19
|
+
afterEach(() => {
|
20
|
+
jest.clearAllMocks();
|
21
|
+
});
|
22
|
+
it('should render modal correctly', () => {
|
23
|
+
render(
|
24
|
+
<TermsOfUse
|
25
|
+
isModalOpen
|
26
|
+
onSecondaryAction={onSecondaryAction}
|
27
|
+
handleModalToggle={handleModalToggle}
|
28
|
+
ouiaId="Terms"
|
29
|
+
>
|
30
|
+
{body}
|
31
|
+
</TermsOfUse>
|
32
|
+
);
|
33
|
+
expect(screen.getByRole('heading', { name: /Terms of use/i })).toBeTruthy();
|
34
|
+
expect(screen.getByRole('button', { name: /Accept/i })).toBeTruthy();
|
35
|
+
expect(screen.getByRole('button', { name: /Decline/i })).toBeTruthy();
|
36
|
+
expect(screen.getByRole('heading', { name: /Heading 1/i })).toBeTruthy();
|
37
|
+
expect(screen.getByText(/Legal text/i)).toBeTruthy();
|
38
|
+
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal');
|
39
|
+
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal--default');
|
40
|
+
});
|
41
|
+
it('should handle image and altText props', () => {
|
42
|
+
render(
|
43
|
+
<TermsOfUse
|
44
|
+
isModalOpen
|
45
|
+
onSecondaryAction={onSecondaryAction}
|
46
|
+
handleModalToggle={handleModalToggle}
|
47
|
+
image="./image.png"
|
48
|
+
altText="Test image"
|
49
|
+
>
|
50
|
+
{body}
|
51
|
+
</TermsOfUse>
|
52
|
+
);
|
53
|
+
expect(screen.getByRole('img')).toBeTruthy();
|
54
|
+
expect(screen.getByRole('img')).toHaveAttribute('alt', 'Test image');
|
55
|
+
});
|
56
|
+
it('should handle className prop', () => {
|
57
|
+
render(
|
58
|
+
<TermsOfUse
|
59
|
+
isModalOpen
|
60
|
+
onSecondaryAction={onSecondaryAction}
|
61
|
+
handleModalToggle={handleModalToggle}
|
62
|
+
className="test"
|
63
|
+
>
|
64
|
+
{body}
|
65
|
+
</TermsOfUse>
|
66
|
+
);
|
67
|
+
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal');
|
68
|
+
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal--default');
|
69
|
+
expect(screen.getByRole('dialog')).toHaveClass('test');
|
70
|
+
});
|
71
|
+
it('should handle title prop', () => {
|
72
|
+
render(
|
73
|
+
<TermsOfUse
|
74
|
+
isModalOpen
|
75
|
+
onSecondaryAction={onSecondaryAction}
|
76
|
+
handleModalToggle={handleModalToggle}
|
77
|
+
title="Updated title"
|
78
|
+
>
|
79
|
+
{body}
|
80
|
+
</TermsOfUse>
|
81
|
+
);
|
82
|
+
expect(screen.getByRole('heading', { name: /Updated title/i })).toBeTruthy();
|
83
|
+
expect(screen.queryByRole('heading', { name: /Terms of use/i })).toBeFalsy();
|
84
|
+
});
|
85
|
+
it('should handle primary button prop', () => {
|
86
|
+
render(
|
87
|
+
<TermsOfUse
|
88
|
+
isModalOpen
|
89
|
+
onSecondaryAction={onSecondaryAction}
|
90
|
+
handleModalToggle={handleModalToggle}
|
91
|
+
primaryActionBtn="First"
|
92
|
+
>
|
93
|
+
{body}
|
94
|
+
</TermsOfUse>
|
95
|
+
);
|
96
|
+
expect(screen.getByRole('button', { name: /First/i })).toBeTruthy();
|
97
|
+
expect(screen.queryByRole('button', { name: /Accept/i })).toBeFalsy();
|
98
|
+
});
|
99
|
+
it('should handle secondary button prop', () => {
|
100
|
+
render(
|
101
|
+
<TermsOfUse
|
102
|
+
isModalOpen
|
103
|
+
onSecondaryAction={onSecondaryAction}
|
104
|
+
handleModalToggle={handleModalToggle}
|
105
|
+
secondaryActionBtn="Second"
|
106
|
+
>
|
107
|
+
{body}
|
108
|
+
</TermsOfUse>
|
109
|
+
);
|
110
|
+
expect(screen.getByRole('button', { name: /Second/i })).toBeTruthy();
|
111
|
+
expect(screen.queryByRole('button', { name: /Deny/i })).toBeFalsy();
|
112
|
+
});
|
113
|
+
it('should handle primary button click', async () => {
|
114
|
+
render(
|
115
|
+
<TermsOfUse
|
116
|
+
isModalOpen
|
117
|
+
onPrimaryAction={onPrimaryAction}
|
118
|
+
onSecondaryAction={onSecondaryAction}
|
119
|
+
handleModalToggle={handleModalToggle}
|
120
|
+
>
|
121
|
+
{body}
|
122
|
+
</TermsOfUse>
|
123
|
+
);
|
124
|
+
await userEvent.click(screen.getByRole('button', { name: /Accept/i }));
|
125
|
+
expect(onPrimaryAction).toHaveBeenCalledTimes(1);
|
126
|
+
expect(handleModalToggle).toHaveBeenCalledTimes(1);
|
127
|
+
});
|
128
|
+
it('should handle secondary button click', async () => {
|
129
|
+
render(
|
130
|
+
<TermsOfUse isModalOpen onSecondaryAction={onSecondaryAction} handleModalToggle={handleModalToggle}>
|
131
|
+
{body}
|
132
|
+
</TermsOfUse>
|
133
|
+
);
|
134
|
+
await userEvent.click(screen.getByRole('button', { name: /Decline/i }));
|
135
|
+
expect(onSecondaryAction).toHaveBeenCalledTimes(1);
|
136
|
+
expect(handleModalToggle).not.toHaveBeenCalled();
|
137
|
+
});
|
138
|
+
});
|
@@ -0,0 +1,117 @@
|
|
1
|
+
// ============================================================================
|
2
|
+
// Terms of Use Modal - Chatbot Modal Extension
|
3
|
+
// ============================================================================
|
4
|
+
import React from 'react';
|
5
|
+
import { Button, Content, ModalBody, ModalFooter, ModalHeader, ModalProps } from '@patternfly/react-core';
|
6
|
+
import { ChatbotDisplayMode } from '../Chatbot';
|
7
|
+
import ChatbotModal from '../ChatbotModal/ChatbotModal';
|
8
|
+
|
9
|
+
export interface TermsOfUseProps extends ModalProps {
|
10
|
+
/** Class applied to modal */
|
11
|
+
className?: string;
|
12
|
+
/** Action assigned to primary modal button */
|
13
|
+
onPrimaryAction?: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
|
14
|
+
/** Action assigned to secondary modal button */
|
15
|
+
onSecondaryAction: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
|
16
|
+
/** Name of primary modal button */
|
17
|
+
primaryActionBtn?: string;
|
18
|
+
/** Name of secondary modal button */
|
19
|
+
secondaryActionBtn?: string;
|
20
|
+
/** Function that handles modal toggle */
|
21
|
+
handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
|
22
|
+
/** Whether modal is open */
|
23
|
+
isModalOpen: boolean;
|
24
|
+
/** Title of modal */
|
25
|
+
title?: string;
|
26
|
+
/** Display mode for the Chatbot parent; this influences the styles applied */
|
27
|
+
displayMode?: ChatbotDisplayMode;
|
28
|
+
/** Optional image displayed in header */
|
29
|
+
image?: string;
|
30
|
+
/** Alt text for optional image displayed in header */
|
31
|
+
altText?: string;
|
32
|
+
/** Ref applied to modal */
|
33
|
+
innerRef?: React.Ref<HTMLDivElement>;
|
34
|
+
/** OuiaID applied to modal */
|
35
|
+
ouiaId?: string;
|
36
|
+
}
|
37
|
+
|
38
|
+
export const TermsOfUseBase: React.FunctionComponent<TermsOfUseProps> = ({
|
39
|
+
handleModalToggle,
|
40
|
+
isModalOpen,
|
41
|
+
onPrimaryAction,
|
42
|
+
onSecondaryAction,
|
43
|
+
primaryActionBtn = 'Accept',
|
44
|
+
secondaryActionBtn = 'Decline',
|
45
|
+
title = 'Terms of use',
|
46
|
+
image,
|
47
|
+
altText,
|
48
|
+
displayMode = ChatbotDisplayMode.default,
|
49
|
+
className,
|
50
|
+
children,
|
51
|
+
innerRef,
|
52
|
+
ouiaId = 'TermsOfUse',
|
53
|
+
...props
|
54
|
+
}: TermsOfUseProps) => {
|
55
|
+
const handlePrimaryAction = (_event: React.MouseEvent | MouseEvent | KeyboardEvent) => {
|
56
|
+
handleModalToggle(_event);
|
57
|
+
onPrimaryAction && onPrimaryAction(_event);
|
58
|
+
};
|
59
|
+
|
60
|
+
const handleSecondaryAction = (_event: React.MouseEvent | MouseEvent | KeyboardEvent) => {
|
61
|
+
onSecondaryAction(_event);
|
62
|
+
};
|
63
|
+
|
64
|
+
const modal = (
|
65
|
+
<ChatbotModal
|
66
|
+
isOpen={isModalOpen}
|
67
|
+
ouiaId={ouiaId}
|
68
|
+
aria-labelledby="terms-of-use-title"
|
69
|
+
aria-describedby="terms-of-use-modal"
|
70
|
+
className={`pf-chatbot__terms-of-use-modal pf-chatbot__terms-of-use-modal--${displayMode} ${className ? className : ''}`}
|
71
|
+
displayMode={displayMode}
|
72
|
+
{...props}
|
73
|
+
>
|
74
|
+
{/* This is a workaround since the PatternFly modal doesn't have ref forwarding */}
|
75
|
+
<section className={`pf-chatbot__terms-of-use--section`} aria-label={title} tabIndex={-1} ref={innerRef}>
|
76
|
+
<ModalHeader>
|
77
|
+
<div className="pf-chatbot__terms-of-use--header">
|
78
|
+
{image && altText && <img src={image} className="pf-chatbot__terms-of-use--image" alt={altText} />}
|
79
|
+
<h1 className="pf-chatbot__terms-of-use--title">{title}</h1>
|
80
|
+
</div>
|
81
|
+
</ModalHeader>
|
82
|
+
<ModalBody>
|
83
|
+
<Content>{children}</Content>
|
84
|
+
</ModalBody>
|
85
|
+
<ModalFooter className="pf-chatbot__terms-of-use--footer">
|
86
|
+
<Button
|
87
|
+
isBlock
|
88
|
+
key="terms-of-use-modal-primary"
|
89
|
+
variant="primary"
|
90
|
+
onClick={handlePrimaryAction}
|
91
|
+
form="terms-of-use-form"
|
92
|
+
size="lg"
|
93
|
+
>
|
94
|
+
{primaryActionBtn}
|
95
|
+
</Button>
|
96
|
+
<Button
|
97
|
+
isBlock
|
98
|
+
key="terms-of-use-modal-secondary"
|
99
|
+
variant="secondary"
|
100
|
+
onClick={handleSecondaryAction}
|
101
|
+
size="lg"
|
102
|
+
>
|
103
|
+
{secondaryActionBtn}
|
104
|
+
</Button>
|
105
|
+
</ModalFooter>
|
106
|
+
</section>
|
107
|
+
</ChatbotModal>
|
108
|
+
);
|
109
|
+
|
110
|
+
return modal;
|
111
|
+
};
|
112
|
+
|
113
|
+
const TermsOfUse = React.forwardRef((props: TermsOfUseProps, ref: React.Ref<HTMLDivElement>) => (
|
114
|
+
<TermsOfUseBase innerRef={ref} {...props} />
|
115
|
+
));
|
116
|
+
|
117
|
+
export default TermsOfUse;
|
package/src/index.ts
CHANGED
package/src/main.scss
CHANGED
@@ -24,6 +24,7 @@
|
|
24
24
|
@import './ResponseActions/ResponseActions';
|
25
25
|
@import './SourcesCard/SourcesCard.scss';
|
26
26
|
@import './SourceDetailsMenuItem/SourceDetailsMenuItem';
|
27
|
+
@import './TermsOfUse/TermsOfUse';
|
27
28
|
|
28
29
|
:where(:root) {
|
29
30
|
// ============================================================================
|