@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.
Files changed (52) hide show
  1. package/dist/cjs/ResponseActions/ResponseActionButton.d.ts +6 -0
  2. package/dist/cjs/ResponseActions/ResponseActionButton.js +10 -2
  3. package/dist/cjs/ResponseActions/ResponseActionButton.test.d.ts +1 -0
  4. package/dist/cjs/ResponseActions/ResponseActionButton.test.js +54 -0
  5. package/dist/cjs/ResponseActions/ResponseActions.d.ts +4 -0
  6. package/dist/cjs/ResponseActions/ResponseActions.js +26 -9
  7. package/dist/cjs/ResponseActions/ResponseActions.test.js +79 -5
  8. package/dist/cjs/TermsOfUse/TermsOfUse.d.ts +34 -0
  9. package/dist/cjs/TermsOfUse/TermsOfUse.js +49 -0
  10. package/dist/cjs/TermsOfUse/TermsOfUse.test.d.ts +1 -0
  11. package/dist/cjs/TermsOfUse/TermsOfUse.test.js +79 -0
  12. package/dist/cjs/TermsOfUse/index.d.ts +2 -0
  13. package/dist/cjs/TermsOfUse/index.js +23 -0
  14. package/dist/cjs/index.d.ts +2 -0
  15. package/dist/cjs/index.js +4 -1
  16. package/dist/css/main.css +66 -4
  17. package/dist/css/main.css.map +1 -1
  18. package/dist/dynamic/TermsOfUse/package.json +1 -0
  19. package/dist/esm/ResponseActions/ResponseActionButton.d.ts +6 -0
  20. package/dist/esm/ResponseActions/ResponseActionButton.js +10 -2
  21. package/dist/esm/ResponseActions/ResponseActionButton.test.d.ts +1 -0
  22. package/dist/esm/ResponseActions/ResponseActionButton.test.js +49 -0
  23. package/dist/esm/ResponseActions/ResponseActions.d.ts +4 -0
  24. package/dist/esm/ResponseActions/ResponseActions.js +26 -9
  25. package/dist/esm/ResponseActions/ResponseActions.test.js +79 -5
  26. package/dist/esm/TermsOfUse/TermsOfUse.d.ts +34 -0
  27. package/dist/esm/TermsOfUse/TermsOfUse.js +42 -0
  28. package/dist/esm/TermsOfUse/TermsOfUse.test.d.ts +1 -0
  29. package/dist/esm/TermsOfUse/TermsOfUse.test.js +74 -0
  30. package/dist/esm/TermsOfUse/index.d.ts +2 -0
  31. package/dist/esm/TermsOfUse/index.js +2 -0
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.js +2 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +1 -1
  36. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +4 -0
  37. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +13 -2
  38. package/patternfly-docs/content/extensions/chatbot/examples/UI/PF-TermsAndConditionsHeader.svg +148 -0
  39. package/patternfly-docs/content/extensions/chatbot/examples/UI/TermsOfUse.tsx +147 -0
  40. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +14 -0
  41. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +2 -2
  42. package/src/ResponseActions/ResponseActionButton.test.tsx +52 -0
  43. package/src/ResponseActions/ResponseActionButton.tsx +46 -27
  44. package/src/ResponseActions/ResponseActions.scss +10 -8
  45. package/src/ResponseActions/ResponseActions.test.tsx +103 -5
  46. package/src/ResponseActions/ResponseActions.tsx +54 -7
  47. package/src/TermsOfUse/TermsOfUse.scss +66 -0
  48. package/src/TermsOfUse/TermsOfUse.test.tsx +138 -0
  49. package/src/TermsOfUse/TermsOfUse.tsx +117 -0
  50. package/src/TermsOfUse/index.ts +3 -0
  51. package/src/index.ts +3 -0
  52. 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
- onClick={positive.onClick}
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
- onClick={negative.onClick}
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
- onClick={copy.onClick}
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
- onClick={share.onClick}
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
- onClick={listen.onClick}
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
- onClick={additionalActions[action]?.onClick}
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;
@@ -0,0 +1,3 @@
1
+ export { default } from './TermsOfUse';
2
+
3
+ export * from './TermsOfUse';
package/src/index.ts CHANGED
@@ -71,3 +71,6 @@ export * from './SourceDetailsMenuItem';
71
71
 
72
72
  export { default as SourcesCard } from './SourcesCard';
73
73
  export * from './SourcesCard';
74
+
75
+ export { default as TermsOfUse } from './TermsOfUse';
76
+ export * from './TermsOfUse';
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
  // ============================================================================