@patternfly/chatbot 6.3.0-prerelease.17 → 6.3.0-prerelease.19
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/FileDropZone/FileDropZone.d.ts +7 -0
- package/dist/cjs/FileDropZone/FileDropZone.js +2 -2
- package/dist/cjs/FileDropZone/FileDropZone.test.js +26 -0
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.d.ts +20 -2
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +14 -3
- package/dist/cjs/Message/CodeBlockMessage/ExpandableSectionForSyntaxHighlighter.d.ts +62 -0
- package/dist/cjs/Message/CodeBlockMessage/ExpandableSectionForSyntaxHighlighter.js +136 -0
- package/dist/cjs/Message/Message.d.ts +16 -1
- package/dist/cjs/Message/Message.test.js +24 -0
- package/dist/cjs/MessageBar/AttachButton.d.ts +7 -0
- package/dist/cjs/MessageBar/AttachButton.js +3 -2
- package/dist/cjs/MessageBar/AttachButton.test.js +24 -0
- package/dist/cjs/MessageBar/MessageBar.d.ts +7 -0
- package/dist/cjs/MessageBar/MessageBar.js +2 -2
- package/dist/css/main.css +10 -0
- package/dist/css/main.css.map +1 -1
- package/dist/esm/FileDropZone/FileDropZone.d.ts +7 -0
- package/dist/esm/FileDropZone/FileDropZone.js +2 -2
- package/dist/esm/FileDropZone/FileDropZone.test.js +26 -0
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.d.ts +20 -2
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +15 -4
- package/dist/esm/Message/CodeBlockMessage/ExpandableSectionForSyntaxHighlighter.d.ts +62 -0
- package/dist/esm/Message/CodeBlockMessage/ExpandableSectionForSyntaxHighlighter.js +130 -0
- package/dist/esm/Message/Message.d.ts +16 -1
- package/dist/esm/Message/Message.test.js +24 -0
- package/dist/esm/MessageBar/AttachButton.d.ts +7 -0
- package/dist/esm/MessageBar/AttachButton.js +3 -2
- package/dist/esm/MessageBar/AttachButton.test.js +24 -0
- package/dist/esm/MessageBar/MessageBar.d.ts +7 -0
- package/dist/esm/MessageBar/MessageBar.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/design-guidelines.md +10 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Customizing Messages/Customizing Messages.md +51 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +9 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +9 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +10 -1
- package/patternfly-docs/content/extensions/chatbot/img/quick-response-confirmation.svg +67 -0
- package/src/FileDropZone/FileDropZone.test.tsx +29 -0
- package/src/FileDropZone/FileDropZone.tsx +9 -0
- package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +7 -0
- package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +99 -12
- package/src/Message/CodeBlockMessage/ExpandableSectionForSyntaxHighlighter.tsx +220 -0
- package/src/Message/Message.test.tsx +30 -0
- package/src/Message/Message.tsx +17 -0
- package/src/MessageBar/AttachButton.test.tsx +45 -0
- package/src/MessageBar/AttachButton.tsx +10 -2
- package/src/MessageBar/MessageBar.tsx +10 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
2
2
|
import '@testing-library/jest-dom';
|
3
3
|
import FileDropZone from './FileDropZone';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
4
5
|
|
5
6
|
describe('FileDropZone', () => {
|
6
7
|
it('should render file drop zone', () => {
|
@@ -11,4 +12,32 @@ describe('FileDropZone', () => {
|
|
11
12
|
render(<FileDropZone onFileDrop={jest.fn()}>Hi</FileDropZone>);
|
12
13
|
expect(screen.getByText('Hi')).toBeTruthy();
|
13
14
|
});
|
15
|
+
|
16
|
+
it('should call onFileDrop when file type is accepted', async () => {
|
17
|
+
const onFileDrop = jest.fn();
|
18
|
+
const { container } = render(
|
19
|
+
<FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
|
20
|
+
);
|
21
|
+
|
22
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
23
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
24
|
+
|
25
|
+
await userEvent.upload(fileInput, file);
|
26
|
+
|
27
|
+
expect(onFileDrop).toHaveBeenCalled();
|
28
|
+
});
|
29
|
+
|
30
|
+
it('should not call onFileDrop when file type is not accepted', async () => {
|
31
|
+
const onFileDrop = jest.fn();
|
32
|
+
const { container } = render(
|
33
|
+
<FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
|
34
|
+
);
|
35
|
+
|
36
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
37
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
38
|
+
|
39
|
+
await userEvent.upload(fileInput, file);
|
40
|
+
|
41
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
42
|
+
});
|
14
43
|
});
|
@@ -3,6 +3,7 @@ import type { FunctionComponent } from 'react';
|
|
3
3
|
import { useState } from 'react';
|
4
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
5
5
|
import { UploadIcon } from '@patternfly/react-icons';
|
6
|
+
import { Accept } from 'react-dropzone/.';
|
6
7
|
|
7
8
|
export interface FileDropZoneProps {
|
8
9
|
/** Content displayed when the drop zone is not currently in use */
|
@@ -13,6 +14,12 @@ export interface FileDropZoneProps {
|
|
13
14
|
infoText?: string;
|
14
15
|
/** When files are dropped or uploaded this callback will be called with all accepted files */
|
15
16
|
onFileDrop: (event: DropEvent, data: File[]) => void;
|
17
|
+
/** Specifies the file types accepted by the attachment upload component.
|
18
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
19
|
+
* For example,
|
20
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
21
|
+
**/
|
22
|
+
allowedFileTypes?: Accept;
|
16
23
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
17
24
|
displayMode?: ChatbotDisplayMode;
|
18
25
|
}
|
@@ -22,6 +29,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
22
29
|
className,
|
23
30
|
infoText = 'Maximum file size is 25 MB',
|
24
31
|
onFileDrop,
|
32
|
+
allowedFileTypes,
|
25
33
|
displayMode = ChatbotDisplayMode.default,
|
26
34
|
...props
|
27
35
|
}: FileDropZoneProps) => {
|
@@ -41,6 +49,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
41
49
|
return (
|
42
50
|
<MultipleFileUpload
|
43
51
|
dropzoneProps={{
|
52
|
+
accept: allowedFileTypes,
|
44
53
|
onDrop: () => setShowDropZone(false),
|
45
54
|
...props
|
46
55
|
}}
|
@@ -80,3 +80,10 @@
|
|
80
80
|
background-color: var(--pf-t--global--background--color--tertiary--default);
|
81
81
|
font-size: var(--pf-t--global--font--size--body--default);
|
82
82
|
}
|
83
|
+
|
84
|
+
.pf-chatbot__message-code-toggle {
|
85
|
+
.pf-v6-c-button.pf-m-link {
|
86
|
+
--pf-v6-c-button--m-link--Color: var(--pf-t--global--color--nonstatus--blue--default);
|
87
|
+
--pf-v6-c-button--hover--Color: var(--pf-t--global--color--nonstatus--blue--hover);
|
88
|
+
}
|
89
|
+
}
|
@@ -5,25 +5,68 @@ import { useState, useRef, useId, useCallback, useEffect } from 'react';
|
|
5
5
|
import SyntaxHighlighter from 'react-syntax-highlighter';
|
6
6
|
import { obsidian } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
7
7
|
// Import PatternFly components
|
8
|
-
import {
|
8
|
+
import {
|
9
|
+
CodeBlock,
|
10
|
+
CodeBlockAction,
|
11
|
+
CodeBlockCode,
|
12
|
+
Button,
|
13
|
+
Tooltip,
|
14
|
+
ExpandableSection,
|
15
|
+
ExpandableSectionToggle,
|
16
|
+
ExpandableSectionProps,
|
17
|
+
ExpandableSectionToggleProps,
|
18
|
+
ExpandableSectionVariant
|
19
|
+
} from '@patternfly/react-core';
|
9
20
|
|
10
21
|
import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon';
|
11
22
|
import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon';
|
12
|
-
import {
|
23
|
+
import { ExpandableSectionForSyntaxHighlighter } from './ExpandableSectionForSyntaxHighlighter';
|
24
|
+
|
25
|
+
export interface CodeBlockMessageProps {
|
26
|
+
/** Content rendered in code block */
|
27
|
+
children?: React.ReactNode;
|
28
|
+
/** Aria label applied to code block */
|
29
|
+
'aria-label'?: string;
|
30
|
+
/** Class name applied to code block */
|
31
|
+
className?: string;
|
32
|
+
/** Whether code block is expandable */
|
33
|
+
isExpandable?: boolean;
|
34
|
+
/** Additional props passed to expandable section if isExpandable is applied */
|
35
|
+
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
36
|
+
/** Additional props passed to expandable toggle if isExpandable is applied */
|
37
|
+
expandableSectionToggleProps?: ExpandableSectionToggleProps;
|
38
|
+
/** Link text applied to expandable toggle when expanded */
|
39
|
+
expandedText?: string;
|
40
|
+
/** Link text applied to expandable toggle when collapsed */
|
41
|
+
collapsedText?: string;
|
42
|
+
}
|
13
43
|
|
14
44
|
const CodeBlockMessage = ({
|
15
45
|
children,
|
16
46
|
className,
|
17
47
|
'aria-label': ariaLabel,
|
48
|
+
isExpandable = false,
|
49
|
+
expandableSectionProps,
|
50
|
+
expandableSectionToggleProps,
|
51
|
+
expandedText = 'Show less',
|
52
|
+
collapsedText = 'Show more',
|
18
53
|
...props
|
19
|
-
}:
|
54
|
+
}: CodeBlockMessageProps) => {
|
20
55
|
const [copied, setCopied] = useState(false);
|
56
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
21
57
|
|
22
|
-
const buttonRef = useRef(
|
58
|
+
const buttonRef = useRef();
|
23
59
|
const tooltipID = useId();
|
60
|
+
const toggleId = useId();
|
61
|
+
const contentId = useId();
|
62
|
+
const codeBlockRef = useRef<HTMLDivElement>(null);
|
24
63
|
|
25
64
|
const language = /language-(\w+)/.exec(className || '')?.[1];
|
26
65
|
|
66
|
+
const onToggle = (isExpanded) => {
|
67
|
+
setIsExpanded(isExpanded);
|
68
|
+
};
|
69
|
+
|
27
70
|
// Handle clicking copy button
|
28
71
|
const handleCopy = useCallback((event, text) => {
|
29
72
|
navigator.clipboard.writeText(text.toString());
|
@@ -69,17 +112,61 @@ const CodeBlockMessage = ({
|
|
69
112
|
);
|
70
113
|
|
71
114
|
return (
|
72
|
-
<div className="pf-chatbot__message-code-block">
|
115
|
+
<div className="pf-chatbot__message-code-block" ref={codeBlockRef}>
|
73
116
|
<CodeBlock actions={actions}>
|
74
117
|
<CodeBlockCode>
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
118
|
+
<>
|
119
|
+
{language ? (
|
120
|
+
// SyntaxHighlighter doesn't work with ExpandableSection because it targets the direct child
|
121
|
+
// Forked for now and adjusted to match what we need
|
122
|
+
<ExpandableSectionForSyntaxHighlighter
|
123
|
+
variant={ExpandableSectionVariant.truncate}
|
124
|
+
isExpanded={isExpanded}
|
125
|
+
isDetached
|
126
|
+
toggleId={toggleId}
|
127
|
+
contentId={contentId}
|
128
|
+
language={language}
|
129
|
+
{...expandableSectionProps}
|
130
|
+
>
|
131
|
+
<SyntaxHighlighter
|
132
|
+
{...props}
|
133
|
+
language={language}
|
134
|
+
style={obsidian}
|
135
|
+
PreTag="div"
|
136
|
+
CodeTag="div"
|
137
|
+
wrapLongLines
|
138
|
+
>
|
139
|
+
{String(children).replace(/\n$/, '')}
|
140
|
+
</SyntaxHighlighter>
|
141
|
+
</ExpandableSectionForSyntaxHighlighter>
|
142
|
+
) : (
|
143
|
+
<ExpandableSection
|
144
|
+
variant={ExpandableSectionVariant.truncate}
|
145
|
+
isExpanded={isExpanded}
|
146
|
+
isDetached
|
147
|
+
toggleId={toggleId}
|
148
|
+
contentId={contentId}
|
149
|
+
{...expandableSectionProps}
|
150
|
+
>
|
151
|
+
{children}
|
152
|
+
</ExpandableSection>
|
153
|
+
)}
|
154
|
+
</>
|
82
155
|
</CodeBlockCode>
|
156
|
+
{isExpandable && (
|
157
|
+
<ExpandableSectionToggle
|
158
|
+
isExpanded={isExpanded}
|
159
|
+
onToggle={onToggle}
|
160
|
+
direction="up"
|
161
|
+
toggleId={toggleId}
|
162
|
+
contentId={contentId}
|
163
|
+
hasTruncatedContent
|
164
|
+
className="pf-chatbot__message-code-toggle"
|
165
|
+
{...expandableSectionToggleProps}
|
166
|
+
>
|
167
|
+
{isExpanded ? expandedText : collapsedText}
|
168
|
+
</ExpandableSectionToggle>
|
169
|
+
)}
|
83
170
|
</CodeBlock>
|
84
171
|
</div>
|
85
172
|
);
|
@@ -0,0 +1,220 @@
|
|
1
|
+
import { Component, createRef } from 'react';
|
2
|
+
import styles from '@patternfly/react-styles/css/components/ExpandableSection/expandable-section';
|
3
|
+
import { css } from '@patternfly/react-styles';
|
4
|
+
import lineClamp from '@patternfly/react-tokens/dist/esm/c_expandable_section_m_truncate__content_LineClamp';
|
5
|
+
import { debounce, getResizeObserver, getUniqueId, PickOptional } from '@patternfly/react-core';
|
6
|
+
|
7
|
+
export enum ExpandableSectionVariant {
|
8
|
+
default = 'default',
|
9
|
+
truncate = 'truncate'
|
10
|
+
}
|
11
|
+
|
12
|
+
/** The main expandable section component. */
|
13
|
+
|
14
|
+
export interface ExpandableSectionProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onToggle'> {
|
15
|
+
/** Content rendered inside the expandable section. */
|
16
|
+
children?: React.ReactNode;
|
17
|
+
/** Additional classes added to the expandable section. */
|
18
|
+
className?: string;
|
19
|
+
/** Id of the content of the expandable section. When passing in the isDetached property, this
|
20
|
+
* property's value should match the contentId property of the expandable section toggle sub-component.
|
21
|
+
*/
|
22
|
+
contentId?: string;
|
23
|
+
/** Id of the toggle of the expandable section, which provides an accessible name to the
|
24
|
+
* expandable section content via the aria-labelledby attribute. When the isDetached property
|
25
|
+
* is also passed in, the value of this property must match the toggleId property of the
|
26
|
+
* expandable section toggle sub-component.
|
27
|
+
*/
|
28
|
+
toggleId?: string;
|
29
|
+
/** Display size variant. Set to "lg" for disclosure styling. */
|
30
|
+
displaySize?: 'default' | 'lg';
|
31
|
+
/** Indicates the expandable section has a detached toggle. */
|
32
|
+
isDetached?: boolean;
|
33
|
+
/** Flag to indicate if the content is expanded. */
|
34
|
+
isExpanded?: boolean;
|
35
|
+
/** Flag to indicate if the content is indented. */
|
36
|
+
isIndented?: boolean;
|
37
|
+
/** Flag to indicate the width of the component is limited. Set to "true" for disclosure styling. */
|
38
|
+
isWidthLimited?: boolean;
|
39
|
+
/** Truncates the expandable content to the specified number of lines when using the
|
40
|
+
* "truncate" variant.
|
41
|
+
*/
|
42
|
+
truncateMaxLines?: number;
|
43
|
+
/** Determines the variant of the expandable section. When passing in "truncate" as the
|
44
|
+
* variant, the expandable content will be truncated after 3 lines by default.
|
45
|
+
*/
|
46
|
+
variant?: 'default' | 'truncate';
|
47
|
+
language?: string;
|
48
|
+
}
|
49
|
+
|
50
|
+
interface ExpandableSectionState {
|
51
|
+
isExpanded: boolean;
|
52
|
+
hasToggle: boolean;
|
53
|
+
previousWidth: number | undefined;
|
54
|
+
}
|
55
|
+
|
56
|
+
const setLineClamp = (element: HTMLDivElement | null, lines?: number, language?: string, isExpanded?: boolean) => {
|
57
|
+
if (!element || !lines || lines < 1 || typeof isExpanded === 'undefined') {
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
|
61
|
+
if (language) {
|
62
|
+
const selector = `.language-${language.toLowerCase()}`;
|
63
|
+
const childElement = element.querySelector(selector) as HTMLDivElement;
|
64
|
+
|
65
|
+
if (!childElement) {
|
66
|
+
return;
|
67
|
+
}
|
68
|
+
if (isExpanded) {
|
69
|
+
// Reset all truncation-related styles to their default values
|
70
|
+
childElement.style.removeProperty('-webkit-line-clamp');
|
71
|
+
childElement.style.removeProperty('display');
|
72
|
+
childElement.style.removeProperty('-webkit-box-orient');
|
73
|
+
childElement.style.removeProperty('overflow');
|
74
|
+
} else {
|
75
|
+
childElement.style.setProperty('-webkit-line-clamp', lines.toString());
|
76
|
+
childElement.style.setProperty('display', '-webkit-box');
|
77
|
+
childElement.style.setProperty('-webkit-box-orient', 'vertical');
|
78
|
+
childElement.style.setProperty('overflow', 'hidden');
|
79
|
+
}
|
80
|
+
}
|
81
|
+
};
|
82
|
+
|
83
|
+
class ExpandableSectionForSyntaxHighlighter extends Component<ExpandableSectionProps, ExpandableSectionState> {
|
84
|
+
static displayName = 'ExpandableSection';
|
85
|
+
constructor(props: ExpandableSectionProps) {
|
86
|
+
super(props);
|
87
|
+
|
88
|
+
this.state = {
|
89
|
+
isExpanded: props.isExpanded ?? false,
|
90
|
+
hasToggle: true,
|
91
|
+
previousWidth: undefined
|
92
|
+
};
|
93
|
+
}
|
94
|
+
|
95
|
+
expandableContentRef = createRef<HTMLDivElement>();
|
96
|
+
/* eslint-disable-next-line */
|
97
|
+
observer: any = () => {};
|
98
|
+
|
99
|
+
static defaultProps: PickOptional<ExpandableSectionProps> = {
|
100
|
+
className: '',
|
101
|
+
isDetached: false,
|
102
|
+
displaySize: 'default',
|
103
|
+
isWidthLimited: false,
|
104
|
+
isIndented: false,
|
105
|
+
variant: 'default'
|
106
|
+
};
|
107
|
+
|
108
|
+
componentDidMount() {
|
109
|
+
if (this.props.variant === ExpandableSectionVariant.truncate) {
|
110
|
+
const expandableContent = this.expandableContentRef.current;
|
111
|
+
if (expandableContent) {
|
112
|
+
this.setState({ previousWidth: expandableContent.offsetWidth });
|
113
|
+
this.observer = getResizeObserver(expandableContent, this.handleResize, false);
|
114
|
+
|
115
|
+
if (this.props.truncateMaxLines) {
|
116
|
+
setLineClamp(expandableContent, this.props.truncateMaxLines, this.props.language, this.state.isExpanded);
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
this.checkToggleVisibility();
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
componentDidUpdate(prevProps: ExpandableSectionProps) {
|
125
|
+
if (
|
126
|
+
this.props.variant === ExpandableSectionVariant.truncate &&
|
127
|
+
(prevProps.truncateMaxLines !== this.props.truncateMaxLines ||
|
128
|
+
prevProps.children !== this.props.children ||
|
129
|
+
prevProps.isExpanded !== this.props.isExpanded)
|
130
|
+
) {
|
131
|
+
const expandableContent = this.expandableContentRef.current;
|
132
|
+
setLineClamp(expandableContent, this.props.truncateMaxLines, this.props.language, this.props.isExpanded);
|
133
|
+
this.checkToggleVisibility();
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
componentWillUnmount() {
|
138
|
+
if (this.props.variant === ExpandableSectionVariant.truncate) {
|
139
|
+
this.observer();
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
checkToggleVisibility = () => {
|
144
|
+
if (this.expandableContentRef?.current) {
|
145
|
+
const maxLines = this.props.truncateMaxLines || parseInt(lineClamp.value);
|
146
|
+
const totalLines =
|
147
|
+
this.expandableContentRef.current.scrollHeight /
|
148
|
+
parseInt(getComputedStyle(this.expandableContentRef.current).lineHeight);
|
149
|
+
|
150
|
+
this.setState({
|
151
|
+
hasToggle: totalLines > maxLines
|
152
|
+
});
|
153
|
+
}
|
154
|
+
};
|
155
|
+
|
156
|
+
resize = () => {
|
157
|
+
if (this.expandableContentRef.current) {
|
158
|
+
const { offsetWidth } = this.expandableContentRef.current;
|
159
|
+
if (this.state.previousWidth !== offsetWidth) {
|
160
|
+
this.setState({ previousWidth: offsetWidth });
|
161
|
+
this.checkToggleVisibility();
|
162
|
+
}
|
163
|
+
}
|
164
|
+
};
|
165
|
+
handleResize = debounce(this.resize, 250);
|
166
|
+
|
167
|
+
render() {
|
168
|
+
const {
|
169
|
+
className,
|
170
|
+
children,
|
171
|
+
isExpanded,
|
172
|
+
isDetached,
|
173
|
+
displaySize,
|
174
|
+
isWidthLimited,
|
175
|
+
isIndented,
|
176
|
+
contentId,
|
177
|
+
toggleId,
|
178
|
+
variant,
|
179
|
+
...props
|
180
|
+
} = this.props;
|
181
|
+
|
182
|
+
if (isDetached && !toggleId) {
|
183
|
+
/* eslint-disable no-console */
|
184
|
+
console.warn(
|
185
|
+
'ExpandableSection: The toggleId value must be passed in and must match the toggleId of the ExpandableSectionToggle.'
|
186
|
+
);
|
187
|
+
}
|
188
|
+
|
189
|
+
const uniqueContentId = contentId || getUniqueId('expandable-section-content');
|
190
|
+
const uniqueToggleId = toggleId || getUniqueId('expandable-section-toggle');
|
191
|
+
|
192
|
+
return (
|
193
|
+
<div
|
194
|
+
className={css(
|
195
|
+
styles.expandableSection,
|
196
|
+
isExpanded && styles.modifiers.expanded,
|
197
|
+
displaySize === 'lg' && styles.modifiers.displayLg,
|
198
|
+
isWidthLimited && styles.modifiers.limitWidth,
|
199
|
+
isIndented && styles.modifiers.indented,
|
200
|
+
variant === ExpandableSectionVariant.truncate && styles.modifiers.truncate,
|
201
|
+
className
|
202
|
+
)}
|
203
|
+
{...props}
|
204
|
+
>
|
205
|
+
<div
|
206
|
+
ref={this.expandableContentRef}
|
207
|
+
className={css(styles.expandableSectionContent)}
|
208
|
+
hidden={variant !== ExpandableSectionVariant.truncate && !isExpanded}
|
209
|
+
id={uniqueContentId}
|
210
|
+
aria-labelledby={uniqueToggleId}
|
211
|
+
role="region"
|
212
|
+
>
|
213
|
+
{children}
|
214
|
+
</div>
|
215
|
+
</div>
|
216
|
+
);
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
export { ExpandableSectionForSyntaxHighlighter };
|
@@ -501,6 +501,36 @@ describe('Message', () => {
|
|
501
501
|
screen.getByText(/https:\/\/raw.githubusercontent.com\/Azure-Samples\/helm-charts\/master\/docs/i)
|
502
502
|
).toBeTruthy();
|
503
503
|
});
|
504
|
+
it('should render expandable code correctly', () => {
|
505
|
+
render(
|
506
|
+
<Message avatar="./img" role="user" name="User" content={CODE_MESSAGE} codeBlockProps={{ isExpandable: true }} />
|
507
|
+
);
|
508
|
+
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
|
509
|
+
expect(screen.getByRole('button', { name: 'Copy code' })).toBeTruthy();
|
510
|
+
expect(screen.getByText(/yaml/)).toBeTruthy();
|
511
|
+
expect(screen.getByText(/apiVersion/i)).toBeTruthy();
|
512
|
+
expect(screen.getByRole('button', { name: /Show more/i })).toBeTruthy();
|
513
|
+
});
|
514
|
+
it('should handle click on expandable code correctly', async () => {
|
515
|
+
render(
|
516
|
+
<Message avatar="./img" role="user" name="User" content={CODE_MESSAGE} codeBlockProps={{ isExpandable: true }} />
|
517
|
+
);
|
518
|
+
const button = screen.getByRole('button', { name: /Show more/i });
|
519
|
+
await userEvent.click(button);
|
520
|
+
expect(screen.getByRole('button', { name: /Show less/i })).toBeTruthy();
|
521
|
+
expect(screen.getByText(/yaml/)).toBeTruthy();
|
522
|
+
expect(screen.getByText(/apiVersion:/i)).toBeTruthy();
|
523
|
+
expect(screen.getByText(/helm.openshift.io\/v1beta1/i)).toBeTruthy();
|
524
|
+
expect(screen.getByText(/metadata:/i)).toBeTruthy();
|
525
|
+
expect(screen.getByText(/name:/i)).toBeTruthy();
|
526
|
+
expect(screen.getByText(/azure-sample-repo0oooo00ooo/i)).toBeTruthy();
|
527
|
+
expect(screen.getByText(/spec/i)).toBeTruthy();
|
528
|
+
expect(screen.getByText(/connectionConfig:/i)).toBeTruthy();
|
529
|
+
expect(screen.getByText(/url:/i)).toBeTruthy();
|
530
|
+
expect(
|
531
|
+
screen.getByText(/https:\/\/raw.githubusercontent.com\/Azure-Samples\/helm-charts\/master\/docs/i)
|
532
|
+
).toBeTruthy();
|
533
|
+
});
|
504
534
|
it('can click copy code button', async () => {
|
505
535
|
// need explicit setup since RTL stubs clipboard if you do this
|
506
536
|
const user = userEvent.setup();
|
package/src/Message/Message.tsx
CHANGED
@@ -11,6 +11,8 @@ import {
|
|
11
11
|
AvatarProps,
|
12
12
|
ButtonProps,
|
13
13
|
ContentVariants,
|
14
|
+
ExpandableSectionProps,
|
15
|
+
ExpandableSectionToggleProps,
|
14
16
|
FormProps,
|
15
17
|
Label,
|
16
18
|
LabelGroupProps,
|
@@ -107,9 +109,24 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
|
|
107
109
|
botWord?: string;
|
108
110
|
/** Label for the English "Loading message," displayed to screenreaders when loading a message */
|
109
111
|
loadingWord?: string;
|
112
|
+
/** Props for code blocks */
|
110
113
|
codeBlockProps?: {
|
114
|
+
/** Aria label applied to code blocks */
|
111
115
|
'aria-label'?: string;
|
116
|
+
/** Class name applied to code blocks */
|
112
117
|
className?: string;
|
118
|
+
/** Whether code blocks are expandable */
|
119
|
+
isExpandable?: boolean;
|
120
|
+
/** Length of text initially shown in expandable code blocks; defaults to 10 characters */
|
121
|
+
maxLength?: number;
|
122
|
+
/** Additional props passed to expandable section if isExpandable is applied */
|
123
|
+
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
124
|
+
/** Additional props passed to expandable toggle if isExpandable is applied */
|
125
|
+
expandableSectionToggleProps?: ExpandableSectionToggleProps;
|
126
|
+
/** Link text applied to expandable toggle when expanded */
|
127
|
+
expandedText?: string;
|
128
|
+
/** Link text applied to expandable toggle when collapsed */
|
129
|
+
collapsedText?: string;
|
113
130
|
};
|
114
131
|
/** Props for quick responses */
|
115
132
|
quickResponses?: QuickResponse[];
|
@@ -53,4 +53,49 @@ describe('Attach button', () => {
|
|
53
53
|
render(<AttachButton isCompact data-testid="button" />);
|
54
54
|
expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
|
55
55
|
});
|
56
|
+
|
57
|
+
it('should set correct accept attribute on file input', async () => {
|
58
|
+
render(<AttachButton inputTestId="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} />);
|
59
|
+
await userEvent.click(screen.getByRole('button', { name: 'Attach' }));
|
60
|
+
const input = screen.getByTestId('input') as HTMLInputElement;
|
61
|
+
expect(input).toHaveAttribute('accept', 'text/plain,.txt');
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should call onAttachAccepted when file type is accepted', async () => {
|
65
|
+
const onAttachAccepted = jest.fn();
|
66
|
+
render(
|
67
|
+
<AttachButton
|
68
|
+
inputTestId="input"
|
69
|
+
allowedFileTypes={{ 'text/plain': ['.txt'] }}
|
70
|
+
onAttachAccepted={onAttachAccepted}
|
71
|
+
/>
|
72
|
+
);
|
73
|
+
|
74
|
+
const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
|
75
|
+
const input = screen.getByTestId('input');
|
76
|
+
|
77
|
+
await userEvent.upload(input, file);
|
78
|
+
|
79
|
+
expect(onAttachAccepted).toHaveBeenCalled();
|
80
|
+
const [attachedFile] = onAttachAccepted.mock.calls[0][0];
|
81
|
+
expect(attachedFile).toEqual(file);
|
82
|
+
});
|
83
|
+
|
84
|
+
it('should not call onAttachAccepted when file type is not accepted', async () => {
|
85
|
+
const onAttachAccepted = jest.fn();
|
86
|
+
render(
|
87
|
+
<AttachButton
|
88
|
+
inputTestId="input"
|
89
|
+
allowedFileTypes={{ 'text/plain': ['.txt'] }}
|
90
|
+
onAttachAccepted={onAttachAccepted}
|
91
|
+
/>
|
92
|
+
);
|
93
|
+
|
94
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
95
|
+
const input = screen.getByTestId('input');
|
96
|
+
|
97
|
+
await userEvent.upload(input, file);
|
98
|
+
|
99
|
+
expect(onAttachAccepted).not.toHaveBeenCalled();
|
100
|
+
});
|
56
101
|
});
|
@@ -7,7 +7,7 @@ import { forwardRef } from 'react';
|
|
7
7
|
|
8
8
|
// Import PatternFly components
|
9
9
|
import { Button, ButtonProps, DropEvent, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
|
10
|
-
import { useDropzone } from 'react-dropzone';
|
10
|
+
import { Accept, useDropzone } from 'react-dropzone';
|
11
11
|
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
|
12
12
|
|
13
13
|
export interface AttachButtonProps extends ButtonProps {
|
@@ -15,6 +15,12 @@ export interface AttachButtonProps extends ButtonProps {
|
|
15
15
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
16
16
|
/** Callback function for AttachButton when an attachment is made */
|
17
17
|
onAttachAccepted?: (data: File[], event: DropEvent) => void;
|
18
|
+
/** Specifies the file types accepted by the attachment upload component.
|
19
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
20
|
+
* For example,
|
21
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
22
|
+
**/
|
23
|
+
allowedFileTypes?: Accept;
|
18
24
|
/** Class name for AttachButton */
|
19
25
|
className?: string;
|
20
26
|
/** Props to control if the AttachButton should be disabled */
|
@@ -40,11 +46,13 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
|
|
40
46
|
tooltipContent = 'Attach',
|
41
47
|
inputTestId,
|
42
48
|
isCompact,
|
49
|
+
allowedFileTypes,
|
43
50
|
...props
|
44
51
|
}: AttachButtonProps) => {
|
45
52
|
const { open, getInputProps } = useDropzone({
|
46
53
|
multiple: true,
|
47
|
-
onDropAccepted: onAttachAccepted
|
54
|
+
onDropAccepted: onAttachAccepted,
|
55
|
+
accept: allowedFileTypes
|
48
56
|
});
|
49
57
|
|
50
58
|
return (
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
3
|
+
import { Accept } from 'react-dropzone/.';
|
3
4
|
import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
4
5
|
|
5
6
|
// Import Chatbot components
|
@@ -78,6 +79,12 @@ export interface MessageBarProps extends TextAreaProps {
|
|
78
79
|
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
|
79
80
|
displayMode?: ChatbotDisplayMode;
|
80
81
|
isCompact?: boolean;
|
82
|
+
/** Specifies the file types accepted by the attachment upload component.
|
83
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
84
|
+
* For example,
|
85
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
86
|
+
**/
|
87
|
+
allowedFileTypes?: Accept;
|
81
88
|
}
|
82
89
|
|
83
90
|
export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
@@ -98,6 +105,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
98
105
|
displayMode,
|
99
106
|
value,
|
100
107
|
isCompact = false,
|
108
|
+
allowedFileTypes,
|
101
109
|
...props
|
102
110
|
}: MessageBarProps) => {
|
103
111
|
// Text Input
|
@@ -295,6 +303,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
295
303
|
tooltipContent={buttonProps?.attach?.tooltipContent}
|
296
304
|
isCompact={isCompact}
|
297
305
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
306
|
+
allowedFileTypes={allowedFileTypes}
|
298
307
|
{...buttonProps?.attach?.props}
|
299
308
|
/>
|
300
309
|
)}
|
@@ -306,6 +315,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
306
315
|
inputTestId={buttonProps?.attach?.inputTestId}
|
307
316
|
isCompact={isCompact}
|
308
317
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
318
|
+
allowedFileTypes={allowedFileTypes}
|
309
319
|
{...buttonProps?.attach?.props}
|
310
320
|
/>
|
311
321
|
)}
|