@patternfly/chatbot 6.4.0-prerelease.23 → 6.4.0-prerelease.25

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 (41) hide show
  1. package/dist/cjs/Message/Message.d.ts +2 -0
  2. package/dist/cjs/Message/Message.js +6 -2
  3. package/dist/cjs/Message/Message.test.js +17 -2
  4. package/dist/cjs/SourcesCard/SourcesCard.js +6 -30
  5. package/dist/cjs/SourcesCard/SourcesCard.test.js +2 -211
  6. package/dist/cjs/SourcesCardBase/SourcesCardBase.d.ts +57 -0
  7. package/dist/cjs/SourcesCardBase/SourcesCardBase.js +49 -0
  8. package/dist/cjs/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
  9. package/dist/cjs/SourcesCardBase/SourcesCardBase.test.js +171 -0
  10. package/dist/cjs/SourcesCardBase/index.d.ts +2 -0
  11. package/dist/cjs/SourcesCardBase/index.js +23 -0
  12. package/dist/cjs/index.d.ts +2 -0
  13. package/dist/cjs/index.js +4 -1
  14. package/dist/css/main.css +4 -2
  15. package/dist/css/main.css.map +1 -1
  16. package/dist/dynamic/SourcesCardBase/package.json +1 -0
  17. package/dist/esm/Message/Message.d.ts +2 -0
  18. package/dist/esm/Message/Message.js +6 -2
  19. package/dist/esm/Message/Message.test.js +17 -2
  20. package/dist/esm/SourcesCard/SourcesCard.js +4 -31
  21. package/dist/esm/SourcesCard/SourcesCard.test.js +3 -212
  22. package/dist/esm/SourcesCardBase/SourcesCardBase.d.ts +57 -0
  23. package/dist/esm/SourcesCardBase/SourcesCardBase.js +47 -0
  24. package/dist/esm/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
  25. package/dist/esm/SourcesCardBase/SourcesCardBase.test.js +166 -0
  26. package/dist/esm/SourcesCardBase/index.d.ts +2 -0
  27. package/dist/esm/SourcesCardBase/index.js +2 -0
  28. package/dist/esm/index.d.ts +2 -0
  29. package/dist/esm/index.js +2 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +1 -0
  33. package/src/Message/Message.test.tsx +25 -2
  34. package/src/Message/Message.tsx +9 -0
  35. package/src/SourcesCard/SourcesCard.scss +4 -1
  36. package/src/SourcesCard/SourcesCard.test.tsx +2 -327
  37. package/src/SourcesCard/SourcesCard.tsx +8 -171
  38. package/src/SourcesCardBase/SourcesCardBase.test.tsx +236 -0
  39. package/src/SourcesCardBase/SourcesCardBase.tsx +242 -0
  40. package/src/SourcesCardBase/index.ts +3 -0
  41. package/src/index.ts +3 -0
@@ -0,0 +1,236 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import '@testing-library/jest-dom';
4
+ import SourcesCardBase from './SourcesCardBase';
5
+
6
+ describe('SourcesCardBase', () => {
7
+ it('should render card correctly if one source with only a link is passed in', () => {
8
+ render(<SourcesCardBase sources={[{ link: '' }]} />);
9
+ expect(screen.getByText('Source 1')).toBeTruthy();
10
+ // no buttons or navigation when there is only 1 source
11
+ expect(screen.queryByRole('button')).toBeFalsy();
12
+ expect(screen.queryByText('1/1')).toBeFalsy();
13
+ });
14
+
15
+ it('should render card correctly if one source with a title is passed in', () => {
16
+ render(<SourcesCardBase sources={[{ title: 'How to make an apple pie', link: '' }]} />);
17
+ expect(screen.getByText('How to make an apple pie')).toBeTruthy();
18
+ });
19
+
20
+ it('should render card correctly if one source with a body is passed in', () => {
21
+ render(<SourcesCardBase sources={[{ link: '', body: 'To make an apple pie, you must first...' }]} />);
22
+ expect(screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
23
+ });
24
+
25
+ it('should render card correctly if one source with a title and body is passed in', () => {
26
+ render(
27
+ <SourcesCardBase
28
+ sources={[{ title: 'How to make an apple pie', link: '', body: 'To make an apple pie, you must first...' }]}
29
+ />
30
+ );
31
+ expect(screen.getByText('How to make an apple pie')).toBeTruthy();
32
+ expect(screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
33
+ });
34
+
35
+ it('should render multiple cards correctly', () => {
36
+ render(
37
+ <SourcesCardBase
38
+ sources={[
39
+ { title: 'How to make an apple pie', link: '' },
40
+ { title: 'How to make cookies', link: '' }
41
+ ]}
42
+ />
43
+ );
44
+ expect(screen.getByText('How to make an apple pie')).toBeTruthy();
45
+ expect(screen.getByText('1/2')).toBeTruthy();
46
+ screen.getByRole('button', { name: /Go to previous page/i });
47
+ screen.getByRole('button', { name: /Go to next page/i });
48
+ });
49
+
50
+ it('should navigate between cards correctly', async () => {
51
+ render(
52
+ <SourcesCardBase
53
+ sources={[
54
+ { title: 'How to make an apple pie', link: '' },
55
+ { title: 'How to make cookies', link: '' }
56
+ ]}
57
+ />
58
+ );
59
+ expect(screen.getByText('How to make an apple pie')).toBeTruthy();
60
+ expect(screen.getByText('1/2')).toBeTruthy();
61
+ expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
62
+ await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
63
+ expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
64
+ expect(screen.getByText('How to make cookies')).toBeTruthy();
65
+ expect(screen.getByText('2/2')).toBeTruthy();
66
+ expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
67
+ expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
68
+ });
69
+
70
+ it('should apply className appropriately', () => {
71
+ render(
72
+ <SourcesCardBase
73
+ sources={[
74
+ { title: 'How to make an apple pie', link: '' },
75
+ { title: 'How to make cookies', link: '' }
76
+ ]}
77
+ className="test"
78
+ />
79
+ );
80
+ const element = screen.getByRole('navigation');
81
+ expect(element).toHaveClass('test');
82
+ });
83
+
84
+ it('should disable pagination appropriately', () => {
85
+ render(
86
+ <SourcesCardBase
87
+ sources={[
88
+ { title: 'How to make an apple pie', link: '' },
89
+ { title: 'How to make cookies', link: '' }
90
+ ]}
91
+ isDisabled
92
+ />
93
+ );
94
+ expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
95
+ expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
96
+ });
97
+
98
+ it('should render navigation aria label appropriately', () => {
99
+ render(
100
+ <SourcesCardBase
101
+ sources={[
102
+ { title: 'How to make an apple pie', link: '' },
103
+ { title: 'How to make cookies', link: '' }
104
+ ]}
105
+ />
106
+ );
107
+ expect(screen.getByRole('navigation', { name: /Pagination/i })).toBeTruthy();
108
+ });
109
+
110
+ it('should change paginationAriaLabel appropriately', () => {
111
+ render(
112
+ <SourcesCardBase
113
+ sources={[
114
+ { title: 'How to make an apple pie', link: '' },
115
+ { title: 'How to make cookies', link: '' }
116
+ ]}
117
+ paginationAriaLabel="Navegación"
118
+ />
119
+ );
120
+ expect(screen.getByRole('navigation', { name: /Navegación/i })).toBeTruthy();
121
+ });
122
+
123
+ it('should change toNextPageAriaLabel appropriately', () => {
124
+ render(
125
+ <SourcesCardBase
126
+ sources={[
127
+ { title: 'How to make an apple pie', link: '' },
128
+ { title: 'How to make cookies', link: '' }
129
+ ]}
130
+ toNextPageAriaLabel="Pase a la siguiente página"
131
+ />
132
+ );
133
+ expect(screen.getByRole('button', { name: /Pase a la siguiente página/i })).toBeTruthy();
134
+ });
135
+
136
+ it('should change toPreviousPageAriaLabel appropriately', () => {
137
+ render(
138
+ <SourcesCardBase
139
+ sources={[
140
+ { title: 'How to make an apple pie', link: '' },
141
+ { title: 'How to make cookies', link: '' }
142
+ ]}
143
+ toPreviousPageAriaLabel="Presione para regresar a la página anterior"
144
+ />
145
+ );
146
+ expect(screen.getByRole('button', { name: /Presione para regresar a la página anterior/i })).toBeTruthy();
147
+ });
148
+
149
+ it('should call onNextClick appropriately', async () => {
150
+ const spy = jest.fn();
151
+ render(
152
+ <SourcesCardBase
153
+ sources={[
154
+ { title: 'How to make an apple pie', link: '' },
155
+ { title: 'How to make cookies', link: '' }
156
+ ]}
157
+ onNextClick={spy}
158
+ />
159
+ );
160
+ await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
161
+ expect(spy).toHaveBeenCalled();
162
+ });
163
+
164
+ it('should call onPreviousClick appropriately', async () => {
165
+ const spy = jest.fn();
166
+ render(
167
+ <SourcesCardBase
168
+ sources={[
169
+ { title: 'How to make an apple pie', link: '' },
170
+ { title: 'How to make cookies', link: '' }
171
+ ]}
172
+ onPreviousClick={spy}
173
+ />
174
+ );
175
+ await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
176
+ await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
177
+ expect(spy).toHaveBeenCalled();
178
+ });
179
+
180
+ it('should call onSetPage appropriately', async () => {
181
+ const spy = jest.fn();
182
+ render(
183
+ <SourcesCardBase
184
+ sources={[
185
+ { title: 'How to make an apple pie', link: '' },
186
+ { title: 'How to make cookies', link: '' }
187
+ ]}
188
+ onSetPage={spy}
189
+ />
190
+ );
191
+ await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
192
+ expect(spy).toHaveBeenCalledTimes(1);
193
+ await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
194
+ expect(spy).toHaveBeenCalledTimes(2);
195
+ });
196
+
197
+ it('should handle showMore appropriately', async () => {
198
+ render(
199
+ <SourcesCardBase
200
+ sources={[
201
+ {
202
+ title: 'Getting started with Red Hat OpenShift',
203
+ link: '#',
204
+ body: 'Red Hat OpenShift on IBM Cloud is a managed offering to create your own cluster of compute hosts where you can deploy and manage containerized apps on IBM Cloud ...',
205
+ hasShowMore: true
206
+ },
207
+ {
208
+ title: 'Azure Red Hat OpenShift documentation',
209
+ link: '#',
210
+ body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
211
+ },
212
+ {
213
+ title: 'OKD Documentation: Home',
214
+ link: '#',
215
+ body: 'OKD is a distribution of Kubernetes optimized for continuous application development and multi-tenant deployment. OKD also serves as the upstream code base upon ...'
216
+ }
217
+ ]}
218
+ />
219
+ );
220
+ expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
221
+ });
222
+
223
+ it('should call onClick appropriately', async () => {
224
+ const spy = jest.fn();
225
+ render(<SourcesCardBase sources={[{ title: 'How to make an apple pie', link: '', onClick: spy }]} />);
226
+ await userEvent.click(screen.getByRole('link', { name: /How to make an apple pie/i }));
227
+ expect(spy).toHaveBeenCalled();
228
+ });
229
+
230
+ it('should apply titleProps appropriately', () => {
231
+ render(
232
+ <SourcesCardBase sources={[{ title: 'How to make an apple pie', link: '', titleProps: { className: 'test' } }]} />
233
+ );
234
+ expect(screen.getByRole('link', { name: /How to make an apple pie/i })).toHaveClass('test');
235
+ });
236
+ });
@@ -0,0 +1,242 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Messages - Sources Card
3
+ // ============================================================================
4
+ import type { FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
5
+ import { useState } from 'react';
6
+ // Import PatternFly components
7
+ import {
8
+ Button,
9
+ ButtonProps,
10
+ ButtonVariant,
11
+ Card,
12
+ CardBody,
13
+ CardBodyProps,
14
+ CardFooter,
15
+ CardFooterProps,
16
+ CardProps,
17
+ CardTitle,
18
+ CardTitleProps,
19
+ ExpandableSection,
20
+ ExpandableSectionVariant,
21
+ Icon,
22
+ Truncate,
23
+ TruncateProps
24
+ } from '@patternfly/react-core';
25
+ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
26
+
27
+ export interface SourcesCardBaseProps extends CardProps {
28
+ /** Additional classes for the pagination navigation container. */
29
+ className?: string;
30
+ /** Flag indicating if the pagination is disabled. */
31
+ isDisabled?: boolean;
32
+ /** @deprecated ofWord has been deprecated. Label for the English word "of." */
33
+ ofWord?: string;
34
+ /** Accessible label for the pagination component. */
35
+ paginationAriaLabel?: string;
36
+ /** Content rendered inside the paginated card */
37
+ sources: {
38
+ /** Title of sources card */
39
+ title?: string;
40
+ /** Subtitle of sources card */
41
+ subtitle?: string;
42
+ /** Link to source */
43
+ link: string;
44
+ /** Body of sources card */
45
+ body?: React.ReactNode | string;
46
+ /** Whether link is external */
47
+ isExternal?: boolean;
48
+ /** Whether sources card is expandable */
49
+ hasShowMore?: boolean;
50
+ /** onClick event applied to the title of the Sources card */
51
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
52
+ /** Any additional props applied to the title of the Sources card */
53
+ titleProps?: ButtonProps;
54
+ /** Custom footer applied to the Sources card */
55
+ footer?: React.ReactNode;
56
+ /** Additional props passed to Truncate component */
57
+ truncateProps?: TruncateProps;
58
+ }[];
59
+ /** Accessible label for the button which moves to the next page. */
60
+ toNextPageAriaLabel?: string;
61
+ /** Accessible label for the button which moves to the previous page. */
62
+ toPreviousPageAriaLabel?: string;
63
+ /** Function called when user clicks to navigate to next page. */
64
+ onNextClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
65
+ /** Function called when user clicks to navigate to previous page. */
66
+ onPreviousClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
67
+ /** Function called when page is changed. */
68
+ onSetPage?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => void;
69
+ /** Label for English words "show more" */
70
+ showMoreWords?: string;
71
+ /** Label for English words "show less" */
72
+ showLessWords?: string;
73
+ /** Additional props passed to card title */
74
+ cardTitleProps?: CardTitleProps;
75
+ /** Additional props passed to card body */
76
+ cardBodyProps?: CardBodyProps;
77
+ /** Additional props passed to card footer */
78
+ cardFooterProps?: CardFooterProps;
79
+ }
80
+
81
+ const SourcesCardBase: FunctionComponent<SourcesCardBaseProps> = ({
82
+ className,
83
+ isDisabled,
84
+ paginationAriaLabel = 'Pagination',
85
+ sources,
86
+ toNextPageAriaLabel = 'Go to next page',
87
+ toPreviousPageAriaLabel = 'Go to previous page',
88
+ onNextClick,
89
+ onPreviousClick,
90
+ onSetPage,
91
+ showMoreWords = 'show more',
92
+ showLessWords = 'show less',
93
+ isCompact,
94
+ cardTitleProps,
95
+ cardBodyProps,
96
+ cardFooterProps,
97
+ ...props
98
+ }: SourcesCardBaseProps) => {
99
+ const [page, setPage] = useState(1);
100
+ const [isExpanded, setIsExpanded] = useState(false);
101
+
102
+ const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => {
103
+ setIsExpanded(isExpanded);
104
+ };
105
+
106
+ const handleNewPage = (_evt: ReactMouseEvent | ReactKeyboardEvent | MouseEvent, newPage: number) => {
107
+ setPage(newPage);
108
+ onSetPage && onSetPage(_evt, newPage);
109
+ };
110
+
111
+ const renderTitle = (title?: string, truncateProps?: TruncateProps) => {
112
+ if (title) {
113
+ return <Truncate content={title} {...truncateProps} />;
114
+ }
115
+ return `Source ${page}`;
116
+ };
117
+
118
+ return (
119
+ <div className="pf-chatbot__sources-card-base">
120
+ <Card isCompact={isCompact} className="pf-chatbot__sources-card" {...props}>
121
+ <CardTitle className="pf-chatbot__sources-card-title" {...cardTitleProps}>
122
+ <div className="pf-chatbot__sources-card-title-container">
123
+ <Button
124
+ component="a"
125
+ variant={ButtonVariant.link}
126
+ href={sources[page - 1].link}
127
+ icon={sources[page - 1].isExternal ? <ExternalLinkSquareAltIcon /> : undefined}
128
+ iconPosition="end"
129
+ isInline
130
+ rel={sources[page - 1].isExternal ? 'noreferrer' : undefined}
131
+ target={sources[page - 1].isExternal ? '_blank' : undefined}
132
+ onClick={sources[page - 1].onClick ?? undefined}
133
+ {...sources[page - 1].titleProps}
134
+ >
135
+ {renderTitle(sources[page - 1].title, sources[page - 1].truncateProps)}
136
+ </Button>
137
+ {sources[page - 1].subtitle && (
138
+ <span className="pf-chatbot__sources-card-subtitle">{sources[page - 1].subtitle}</span>
139
+ )}
140
+ </div>
141
+ </CardTitle>
142
+ {sources[page - 1].body && (
143
+ <CardBody
144
+ className={`pf-chatbot__sources-card-body ${sources[page - 1].footer ? 'pf-chatbot__compact-sources-card-body' : undefined}`}
145
+ {...cardBodyProps}
146
+ >
147
+ {sources[page - 1].hasShowMore ? (
148
+ // prevents extra VO announcements of button text - parent Message has aria-live
149
+ <div aria-live="off">
150
+ <ExpandableSection
151
+ variant={ExpandableSectionVariant.truncate}
152
+ toggleText={isExpanded ? showLessWords : showMoreWords}
153
+ onToggle={onToggle}
154
+ isExpanded={isExpanded}
155
+ truncateMaxLines={2}
156
+ >
157
+ {sources[page - 1].body}
158
+ </ExpandableSection>
159
+ </div>
160
+ ) : (
161
+ <div className="pf-chatbot__sources-card-body-text">{sources[page - 1].body}</div>
162
+ )}
163
+ </CardBody>
164
+ )}
165
+ {sources[page - 1].footer ? (
166
+ <CardFooter className="pf-chatbot__sources-card-footer" {...cardFooterProps}>
167
+ {sources[page - 1].footer}
168
+ </CardFooter>
169
+ ) : (
170
+ sources.length > 1 && (
171
+ <CardFooter className="pf-chatbot__sources-card-footer-container" {...cardFooterProps}>
172
+ <div className="pf-chatbot__sources-card-footer">
173
+ <nav
174
+ className={`pf-chatbot__sources-card-footer-buttons ${className}`}
175
+ aria-label={paginationAriaLabel}
176
+ >
177
+ <Button
178
+ variant={ButtonVariant.plain}
179
+ isDisabled={isDisabled || page === 1}
180
+ data-action="previous"
181
+ onClick={(event) => {
182
+ const newPage = page >= 1 ? page - 1 : 1;
183
+ onPreviousClick && onPreviousClick(event, newPage);
184
+ handleNewPage(event, newPage);
185
+ }}
186
+ aria-label={toPreviousPageAriaLabel}
187
+ >
188
+ <Icon iconSize="lg">
189
+ {/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
190
+ <svg
191
+ className="pf-v6-svg"
192
+ viewBox="0 0 280 500"
193
+ fill="currentColor"
194
+ aria-hidden="true"
195
+ role="img"
196
+ width="1em"
197
+ height="1em"
198
+ >
199
+ <path d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"></path>
200
+ </svg>
201
+ </Icon>
202
+ </Button>
203
+ <span aria-hidden="true">
204
+ {page}/{sources.length}
205
+ </span>
206
+ <Button
207
+ variant={ButtonVariant.plain}
208
+ isDisabled={isDisabled || page === sources.length}
209
+ aria-label={toNextPageAriaLabel}
210
+ data-action="next"
211
+ onClick={(event) => {
212
+ const newPage = page + 1 <= sources.length ? page + 1 : sources.length;
213
+ onNextClick && onNextClick(event, newPage);
214
+ handleNewPage(event, newPage);
215
+ }}
216
+ >
217
+ <Icon isInline iconSize="lg">
218
+ {/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
219
+ <svg
220
+ className="pf-v6-svg"
221
+ viewBox="0 0 180 500"
222
+ fill="currentColor"
223
+ aria-hidden="true"
224
+ role="img"
225
+ width="1em"
226
+ height="1em"
227
+ >
228
+ <path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path>
229
+ </svg>
230
+ </Icon>
231
+ </Button>
232
+ </nav>
233
+ </div>
234
+ </CardFooter>
235
+ )
236
+ )}
237
+ </Card>
238
+ </div>
239
+ );
240
+ };
241
+
242
+ export default SourcesCardBase;
@@ -0,0 +1,3 @@
1
+ export { default } from './SourcesCardBase';
2
+
3
+ export * from './SourcesCardBase';
package/src/index.ts CHANGED
@@ -90,6 +90,9 @@ export * from './SourceDetailsMenuItem';
90
90
  export { default as SourcesCard } from './SourcesCard';
91
91
  export * from './SourcesCard';
92
92
 
93
+ export { default as SourcesCardBase } from './SourcesCardBase';
94
+ export * from './SourcesCardBase';
95
+
93
96
  export { default as TermsOfUse } from './TermsOfUse';
94
97
  export * from './TermsOfUse';
95
98