@patternfly/chatbot 6.5.0-prerelease.24 → 6.5.0-prerelease.26
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/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +9 -1
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +9 -2
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +38 -0
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +12 -2
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.test.d.ts +1 -0
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.test.js +131 -0
- package/dist/css/main.css +11 -0
- package/dist/css/main.css.map +1 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +9 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +10 -3
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +38 -0
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +12 -2
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.test.d.ts +1 -0
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.test.js +126 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx +198 -0
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +12 -2
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +18 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +95 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +51 -15
- package/src/Message/CodeBlockMessage/CodeBlockMessage.test.tsx +171 -0
- package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +12 -3
|
@@ -8,6 +8,7 @@ import { useRef, Fragment } from 'react';
|
|
|
8
8
|
import {
|
|
9
9
|
Button,
|
|
10
10
|
ButtonProps,
|
|
11
|
+
Divider,
|
|
11
12
|
Drawer,
|
|
12
13
|
DrawerPanelContent,
|
|
13
14
|
DrawerContent,
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
DrawerActions,
|
|
18
19
|
DrawerCloseButton,
|
|
19
20
|
DrawerContentBody,
|
|
21
|
+
InputGroup,
|
|
22
|
+
InputGroupItem,
|
|
20
23
|
SearchInput,
|
|
21
24
|
Title,
|
|
22
25
|
DrawerPanelContentProps,
|
|
@@ -28,10 +31,10 @@ import {
|
|
|
28
31
|
DrawerPanelBodyProps,
|
|
29
32
|
SkeletonProps,
|
|
30
33
|
Icon,
|
|
31
|
-
MenuProps,
|
|
32
34
|
TitleProps,
|
|
33
|
-
MenuListProps,
|
|
34
35
|
SearchInputProps,
|
|
36
|
+
MenuProps,
|
|
37
|
+
MenuListProps,
|
|
35
38
|
MenuList,
|
|
36
39
|
MenuGroup,
|
|
37
40
|
MenuItem,
|
|
@@ -125,6 +128,8 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
|
|
|
125
128
|
drawerCloseButtonProps?: DrawerCloseButtonProps;
|
|
126
129
|
/** Additional props appleid to drawer panel body */
|
|
127
130
|
drawerPanelBodyProps?: DrawerPanelBodyProps;
|
|
131
|
+
/** Flag indicating whether a divider should render between the drawer head and title. */
|
|
132
|
+
hasDrawerHeadDivider?: boolean;
|
|
128
133
|
/** Whether to show drawer loading state */
|
|
129
134
|
isLoading?: boolean;
|
|
130
135
|
/** Additional props for loading state */
|
|
@@ -145,6 +150,12 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
|
|
|
145
150
|
navTitleProps?: Partial<TitleProps>;
|
|
146
151
|
/** Visually hidden text that gets announced by assistive technologies. Should be used to convey the result count when the search input value changes. */
|
|
147
152
|
searchInputScreenReaderText?: string;
|
|
153
|
+
/** Custom action rendered before the search input. */
|
|
154
|
+
searchActionStart?: React.ReactNode;
|
|
155
|
+
/** Custom action rendered after the search input. */
|
|
156
|
+
searchActionEnd?: React.ReactNode;
|
|
157
|
+
/** A custom search toolbar to render below the title. This will override the default search actions and/or search input. */
|
|
158
|
+
searchToolbar?: React.ReactNode;
|
|
148
159
|
/** Additional props passed to MenuContent */
|
|
149
160
|
menuContentProps?: Omit<MenuContentProps, 'ref'>;
|
|
150
161
|
}
|
|
@@ -175,6 +186,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
|
|
|
175
186
|
drawerActionsProps,
|
|
176
187
|
drawerCloseButtonProps,
|
|
177
188
|
drawerPanelBodyProps,
|
|
189
|
+
hasDrawerHeadDivider,
|
|
178
190
|
isLoading,
|
|
179
191
|
loadingState,
|
|
180
192
|
errorState,
|
|
@@ -185,6 +197,9 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
|
|
|
185
197
|
navTitleProps,
|
|
186
198
|
navTitleIcon = <OutlinedClockIcon />,
|
|
187
199
|
searchInputScreenReaderText,
|
|
200
|
+
searchActionStart,
|
|
201
|
+
searchActionEnd,
|
|
202
|
+
searchToolbar,
|
|
188
203
|
menuProps,
|
|
189
204
|
menuGroupProps,
|
|
190
205
|
menuContentProps,
|
|
@@ -287,6 +302,38 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
|
|
|
287
302
|
</>
|
|
288
303
|
);
|
|
289
304
|
|
|
305
|
+
const searchInputContainer = handleTextInputChange && (
|
|
306
|
+
<div className="pf-chatbot__input">
|
|
307
|
+
<SearchInput
|
|
308
|
+
aria-label={searchInputAriaLabel}
|
|
309
|
+
onChange={(_event, value) => handleTextInputChange(value)}
|
|
310
|
+
placeholder={searchInputPlaceholder}
|
|
311
|
+
{...searchInputProps}
|
|
312
|
+
/>
|
|
313
|
+
{searchInputScreenReaderText && (
|
|
314
|
+
<div className="pf-chatbot__filter-announcement pf-chatbot-m-hidden">{searchInputScreenReaderText}</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const renderSearchAndActions = () => {
|
|
320
|
+
if (searchToolbar) {
|
|
321
|
+
return searchToolbar;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return searchActionStart || searchActionEnd ? (
|
|
325
|
+
<div className="pf-chatbot__history-search-actions">
|
|
326
|
+
<InputGroup>
|
|
327
|
+
{searchActionStart && <InputGroupItem>{searchActionStart}</InputGroupItem>}
|
|
328
|
+
{searchInputContainer && <InputGroupItem isFill>{searchInputContainer}</InputGroupItem>}
|
|
329
|
+
{searchActionEnd && <InputGroupItem>{searchActionEnd}</InputGroupItem>}
|
|
330
|
+
</InputGroup>
|
|
331
|
+
</div>
|
|
332
|
+
) : (
|
|
333
|
+
searchInputContainer
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
290
337
|
const renderPanelContent = () => {
|
|
291
338
|
const drawer = (
|
|
292
339
|
<>
|
|
@@ -309,6 +356,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
|
|
|
309
356
|
)}
|
|
310
357
|
</DrawerActions>
|
|
311
358
|
</DrawerHead>
|
|
359
|
+
{hasDrawerHeadDivider && <Divider className="pf-chatbot__heading-divider" />}
|
|
312
360
|
<div className="pf-chatbot__heading-container">
|
|
313
361
|
<div className="pf-chatbot__title-container">
|
|
314
362
|
<Icon size="lg" className="pf-chatbot__title-icon">
|
|
@@ -318,19 +366,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
|
|
|
318
366
|
{title}
|
|
319
367
|
</Title>
|
|
320
368
|
</div>
|
|
321
|
-
{!isLoading &&
|
|
322
|
-
<div className="pf-chatbot__input">
|
|
323
|
-
<SearchInput
|
|
324
|
-
aria-label={searchInputAriaLabel}
|
|
325
|
-
onChange={(_event, value) => handleTextInputChange(value)}
|
|
326
|
-
placeholder={searchInputPlaceholder}
|
|
327
|
-
{...searchInputProps}
|
|
328
|
-
/>
|
|
329
|
-
{searchInputScreenReaderText && (
|
|
330
|
-
<div className="pf-chatbot__filter-announcement pf-chatbot-m-hidden">{searchInputScreenReaderText}</div>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
369
|
+
{!isLoading && renderSearchAndActions()}
|
|
334
370
|
</div>
|
|
335
371
|
{isLoading ? <LoadingState {...loadingState} /> : renderDrawerContent()}
|
|
336
372
|
</>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import CodeBlockMessage from './CodeBlockMessage';
|
|
5
|
+
|
|
6
|
+
// Mock clipboard API
|
|
7
|
+
Object.assign(navigator, {
|
|
8
|
+
clipboard: {
|
|
9
|
+
writeText: jest.fn()
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('CodeBlockMessage', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should render inline code for single-line content', () => {
|
|
19
|
+
render(<CodeBlockMessage className="language-javascript">const x = 5;</CodeBlockMessage>);
|
|
20
|
+
const code = screen.getByText('const x = 5;');
|
|
21
|
+
expect(code.tagName).toBe('CODE');
|
|
22
|
+
expect(code).toHaveClass('pf-chatbot__message-inline-code');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render code block for multi-line content', () => {
|
|
26
|
+
const multilineCode = 'const x = 5;\nconst y = 10;';
|
|
27
|
+
const { container } = render(<CodeBlockMessage className="language-javascript">{multilineCode}</CodeBlockMessage>);
|
|
28
|
+
const codeElement = container.querySelector('code');
|
|
29
|
+
expect(codeElement?.textContent).toBe(multilineCode);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should display language label', () => {
|
|
33
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
34
|
+
render(<CodeBlockMessage className="language-javascript">{code}</CodeBlockMessage>);
|
|
35
|
+
expect(screen.getByText('javascript')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should render copy button', () => {
|
|
39
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
40
|
+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
|
|
41
|
+
expect(screen.getByRole('button', { name: 'Copy code' })).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should copy plain string content to clipboard', async () => {
|
|
45
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
46
|
+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
|
|
47
|
+
|
|
48
|
+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
|
|
49
|
+
await userEvent.click(copyButton);
|
|
50
|
+
|
|
51
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should extract text content from React elements when copying', async () => {
|
|
55
|
+
// Simulate what happens with syntax highlighting - children become React elements
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<CodeBlockMessage className="language-javascript">
|
|
58
|
+
<span className="hljs-keyword">const</span> x = 5;{'\n'}
|
|
59
|
+
<span className="hljs-keyword">const</span> y = 10;
|
|
60
|
+
</CodeBlockMessage>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
|
|
64
|
+
await userEvent.click(copyButton);
|
|
65
|
+
|
|
66
|
+
// Should extract actual text content from DOM, not "[object Object]"
|
|
67
|
+
const codeElement = container.querySelector('code');
|
|
68
|
+
const expectedText = codeElement?.textContent || '';
|
|
69
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedText);
|
|
70
|
+
expect(expectedText).not.toContain('[object Object]');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should show check icon after copying', async () => {
|
|
74
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
75
|
+
render(<CodeBlockMessage>{code}</CodeBlockMessage>);
|
|
76
|
+
|
|
77
|
+
const copyButton = screen.getByRole('button', { name: 'Copy code' });
|
|
78
|
+
await userEvent.click(copyButton);
|
|
79
|
+
|
|
80
|
+
// Check icon should be visible (we can verify by checking if CopyIcon is not present)
|
|
81
|
+
const svgElement = copyButton.querySelector('svg');
|
|
82
|
+
expect(svgElement).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should render expandable section when isExpandable is true', () => {
|
|
86
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
87
|
+
render(<CodeBlockMessage isExpandable>{code}</CodeBlockMessage>);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByRole('button', { name: 'Show more' })).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should toggle expandable section', async () => {
|
|
93
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
94
|
+
render(<CodeBlockMessage isExpandable>{code}</CodeBlockMessage>);
|
|
95
|
+
|
|
96
|
+
const toggleButton = screen.getByRole('button', { name: 'Show more' });
|
|
97
|
+
await userEvent.click(toggleButton);
|
|
98
|
+
|
|
99
|
+
expect(screen.getByRole('button', { name: 'Show less' })).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should use custom expanded/collapsed text', () => {
|
|
103
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
104
|
+
render(
|
|
105
|
+
<CodeBlockMessage isExpandable expandedText="Hide" collapsedText="Reveal">
|
|
106
|
+
{code}
|
|
107
|
+
</CodeBlockMessage>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(screen.getByRole('button', { name: 'Reveal' })).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should pass through expandableSectionProps', () => {
|
|
114
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
115
|
+
const { container } = render(
|
|
116
|
+
<CodeBlockMessage isExpandable expandableSectionProps={{ className: 'custom-expandable-class' }}>
|
|
117
|
+
{code}
|
|
118
|
+
</CodeBlockMessage>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const expandableSection = container.querySelector('.pf-v6-c-expandable-section.custom-expandable-class');
|
|
122
|
+
expect(expandableSection).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should render custom actions', () => {
|
|
126
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
127
|
+
const customAction = <button aria-label="Custom action">Custom</button>;
|
|
128
|
+
render(<CodeBlockMessage customActions={customAction}>{code}</CodeBlockMessage>);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByRole('button', { name: 'Custom action' })).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should apply isPrimary class to inline code', () => {
|
|
134
|
+
render(<CodeBlockMessage isPrimary>const x = 5;</CodeBlockMessage>);
|
|
135
|
+
const code = screen.getByText('const x = 5;');
|
|
136
|
+
expect(code).toHaveClass('pf-m-primary');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should apply shouldRetainStyles class to code block', () => {
|
|
140
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
141
|
+
const { container } = render(<CodeBlockMessage shouldRetainStyles>{code}</CodeBlockMessage>);
|
|
142
|
+
|
|
143
|
+
const codeBlockDiv = container.querySelector('.pf-chatbot__message-code-block');
|
|
144
|
+
expect(codeBlockDiv).toHaveClass('pf-m-markdown');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should use custom aria-label for copy button', () => {
|
|
148
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
149
|
+
render(<CodeBlockMessage aria-label="Copy this code">{code}</CodeBlockMessage>);
|
|
150
|
+
|
|
151
|
+
expect(screen.getByRole('button', { name: 'Copy this code' })).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should prioritize data-expanded-text over expandedText prop', () => {
|
|
155
|
+
const code = 'const x = 5;\nconst y = 10;';
|
|
156
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn());
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
<CodeBlockMessage isExpandable expandedText="Custom Expanded" data-expanded-text="Data Expanded">
|
|
160
|
+
{code}
|
|
161
|
+
</CodeBlockMessage>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
165
|
+
'Message:',
|
|
166
|
+
expect.stringContaining('data-expanded-text or data-collapsed-text will override')
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
consoleErrorSpy.mockRestore();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -92,13 +92,22 @@ const CodeBlockMessage = ({
|
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const onToggle = (isExpanded) => {
|
|
95
|
+
const onToggle = (isExpanded: boolean) => {
|
|
96
96
|
setIsExpanded(isExpanded);
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
// Handle clicking copy button
|
|
100
|
-
const handleCopy = useCallback((
|
|
101
|
-
|
|
100
|
+
const handleCopy = useCallback((_event: React.MouseEvent, text: React.ReactNode) => {
|
|
101
|
+
let textToCopy = '';
|
|
102
|
+
if (typeof text === 'string') {
|
|
103
|
+
textToCopy = text;
|
|
104
|
+
} else {
|
|
105
|
+
if (codeBlockRef.current) {
|
|
106
|
+
const codeElement = codeBlockRef.current.querySelector('code');
|
|
107
|
+
textToCopy = codeElement?.textContent || '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
navigator.clipboard.writeText(textToCopy);
|
|
102
111
|
setCopied(true);
|
|
103
112
|
}, []);
|
|
104
113
|
|