Skip to content

Commit c0241c0

Browse files
feat(SourcesCard): Add expanded view (#501)
Allow for cards with more source info. Co-authored-by: Erin Donehoo <edonehoo@redhat.com>
1 parent 9a802bb commit c0241c0

5 files changed

Lines changed: 109 additions & 41 deletions

File tree

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => {
1414
name="Bot"
1515
role="bot"
1616
avatar={patternflyAvatar}
17-
content="Example with sources"
17+
content="This example has a body description that's within the recommended limit of 2 lines:"
1818
sources={{
1919
sources: [
2020
{
@@ -43,7 +43,36 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => {
4343
name="Bot"
4444
role="bot"
4545
avatar={patternflyAvatar}
46-
content="Example with very long sources"
46+
content="This example has a body description that's longer than the recommended limit of 2 lines, with a link to load the rest of the description:"
47+
sources={{
48+
sources: [
49+
{
50+
title: 'Getting started with Red Hat OpenShift',
51+
link: '#',
52+
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.',
53+
hasShowMore: true
54+
},
55+
{
56+
title: 'Azure Red Hat OpenShift documentation',
57+
link: '#',
58+
body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure.',
59+
hasShowMore: true
60+
},
61+
{
62+
title: 'OKD Documentation: Home',
63+
link: '#',
64+
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.',
65+
hasShowMore: true
66+
}
67+
],
68+
onSetPage
69+
}}
70+
/>
71+
<Message
72+
name="Bot"
73+
role="bot"
74+
avatar={patternflyAvatar}
75+
content="This example has a truncated title:"
4776
sources={{
4877
sources: [
4978
{
@@ -66,7 +95,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => {
6695
name="Bot"
6796
role="bot"
6897
avatar={patternflyAvatar}
69-
content="Example with only 1 source"
98+
content="This example only includes 1 source:"
7099
sources={{
71100
sources: [
72101
{
@@ -83,7 +112,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => {
83112
name="Bot"
84113
role="bot"
85114
avatar={patternflyAvatar}
86-
content="Example with sources that include a title and link"
115+
content="This example has a title and no body description:"
87116
sources={{
88117
sources: [
89118
{ title: 'Getting started with Red Hat OpenShift', link: '#', isExternal: true },
@@ -105,7 +134,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => {
105134
name="Bot"
106135
role="bot"
107136
avatar={patternflyAvatar}
108-
content="Example with link-only sources (not recommended)"
137+
content="This example displays the source as a link, without a title (not recommended)"
109138
sources={{
110139
sources: [
111140
{

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ If you are using Retrieval-Augmented Generation, you may want to display sources
144144

145145
If a source will open outside of the ChatBot window, add an external link icon via `isExternal`.
146146

147-
The API for a source requires a link at minimum, but we strongly recommend providing a more descriptive title and body description so users have enough context. The title is limited to 1 line and the body is limited to 2 lines.
147+
The API for a source requires a link at minimum, but we strongly recommend providing a more descriptive title and body description so users have enough context. For the best clarity and readability, we strongly recommend limiting the title to 1 line and the body to 2 lines. If the body description is more than 2 lines, use the "long sources" or "very long sources" variant.
148148

149149
```js file="./MessageWithSources.tsx"
150150

packages/module/src/SourcesCard/SourcesCard.scss

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
box-shadow: var(--pf-t--global--box-shadow--sm);
1717
}
1818

19-
.pf-chatbot__sources-card-body {
19+
.pf-chatbot__sources-card-body-text {
2020
display: block;
2121
display: -webkit-box;
2222
height: 2.8125rem;
@@ -25,11 +25,6 @@
2525
-webkit-box-orient: vertical;
2626
overflow: hidden;
2727
text-overflow: ellipsis;
28-
margin-bottom: var(--pf-t--global--spacer--md);
29-
}
30-
31-
.pf-chatbot__sources-card-no-footer {
32-
margin-bottom: var(--pf-t--global--spacer--lg);
3328
}
3429

3530
.pf-chatbot__sources-card-footer-container {
@@ -38,13 +33,14 @@
3833
var(--pf-t--global--spacer--sm) !important;
3934
.pf-chatbot__sources-card-footer {
4035
display: flex;
41-
justify-content: space-between;
4236
align-items: center;
4337

4438
&-buttons {
4539
display: flex;
4640
gap: var(--pf-t--global--spacer--xs);
4741
align-items: center;
42+
justify-content: space-between;
43+
flex: 1;
4844

4945
.pf-v6-c-button {
5046
border-radius: var(--pf-t--global--border--radius--pill);

packages/module/src/SourcesCard/SourcesCard.test.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('SourcesCard', () => {
1111
expect(screen.getByText('Source 1')).toBeTruthy();
1212
// no buttons or navigation when there is only 1 source
1313
expect(screen.queryByRole('button')).toBeFalsy();
14-
expect(screen.queryByText('1 of 1')).toBeFalsy();
14+
expect(screen.queryByText('1/1')).toBeFalsy();
1515
});
1616

1717
it('should render card correctly if one source with a title is passed in', () => {
@@ -48,7 +48,7 @@ describe('SourcesCard', () => {
4848
);
4949
expect(screen.getByText('2 sources')).toBeTruthy();
5050
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
51-
expect(screen.getByText('1 of 2')).toBeTruthy();
51+
expect(screen.getByText('1/2')).toBeTruthy();
5252
screen.getByRole('button', { name: /Go to previous page/i });
5353
screen.getByRole('button', { name: /Go to next page/i });
5454
});
@@ -63,12 +63,12 @@ describe('SourcesCard', () => {
6363
/>
6464
);
6565
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
66-
expect(screen.getByText('1 of 2')).toBeTruthy();
66+
expect(screen.getByText('1/2')).toBeTruthy();
6767
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
6868
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
6969
expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
7070
expect(screen.getByText('How to make cookies')).toBeTruthy();
71-
expect(screen.getByText('2 of 2')).toBeTruthy();
71+
expect(screen.getByText('2/2')).toBeTruthy();
7272
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
7373
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
7474
});
@@ -101,19 +101,6 @@ describe('SourcesCard', () => {
101101
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
102102
});
103103

104-
it('should change ofWord appropriately', () => {
105-
render(
106-
<SourcesCard
107-
sources={[
108-
{ title: 'How to make an apple pie', link: '' },
109-
{ title: 'How to make cookies', link: '' }
110-
]}
111-
ofWord={'de'}
112-
/>
113-
);
114-
expect(screen.getByText('1 de 2')).toBeTruthy();
115-
});
116-
117104
it('should render navigation aria label appropriately', () => {
118105
render(
119106
<SourcesCard
@@ -230,4 +217,30 @@ describe('SourcesCard', () => {
230217
await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
231218
expect(spy).toHaveBeenCalledTimes(2);
232219
});
220+
221+
it('should handle showMore appropriately', async () => {
222+
render(
223+
<SourcesCard
224+
sources={[
225+
{
226+
title: 'Getting started with Red Hat OpenShift',
227+
link: '#',
228+
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 ...',
229+
hasShowMore: true
230+
},
231+
{
232+
title: 'Azure Red Hat OpenShift documentation',
233+
link: '#',
234+
body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
235+
},
236+
{
237+
title: 'OKD Documentation: Home',
238+
link: '#',
239+
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 ...'
240+
}
241+
]}
242+
/>
243+
);
244+
expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
245+
});
233246
});

packages/module/src/SourcesCard/SourcesCard.tsx

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
CardFooter,
1313
CardProps,
1414
CardTitle,
15+
ExpandableSection,
16+
ExpandableSectionVariant,
1517
Icon,
1618
pluralize,
1719
Truncate
@@ -23,12 +25,18 @@ export interface SourcesCardProps extends CardProps {
2325
className?: string;
2426
/** Flag indicating if the pagination is disabled. */
2527
isDisabled?: boolean;
26-
/** Label for the English word "of". */
28+
/** @deprecated ofWord has been deprecated. Label for the English word "of." */
2729
ofWord?: string;
2830
/** Accessible label for the pagination component. */
2931
paginationAriaLabel?: string;
3032
/** Content rendered inside the paginated card */
31-
sources: { title?: string; link: string; body?: React.ReactNode | string; isExternal?: boolean }[];
33+
sources: {
34+
title?: string;
35+
link: string;
36+
body?: React.ReactNode | string;
37+
isExternal?: boolean;
38+
hasShowMore?: boolean;
39+
}[];
3240
/** Label for the English word "source" */
3341
sourceWord?: string;
3442
/** Plural for sourceWord */
@@ -43,12 +51,15 @@ export interface SourcesCardProps extends CardProps {
4351
onPreviousClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
4452
/** Function called when page is changed. */
4553
onSetPage?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => void;
54+
/** Label for English words "show more" */
55+
showMoreWords?: string;
56+
/** Label for English words "show less" */
57+
showLessWords?: string;
4658
}
4759

4860
const SourcesCard: React.FunctionComponent<SourcesCardProps> = ({
4961
className,
5062
isDisabled,
51-
ofWord = 'of',
5263
paginationAriaLabel = 'Pagination',
5364
sources,
5465
sourceWord = 'source',
@@ -58,9 +69,16 @@ const SourcesCard: React.FunctionComponent<SourcesCardProps> = ({
5869
onNextClick,
5970
onPreviousClick,
6071
onSetPage,
72+
showMoreWords = 'show more',
73+
showLessWords = 'show less',
6174
...props
6275
}: SourcesCardProps) => {
6376
const [page, setPage] = React.useState(1);
77+
const [isExpanded, setIsExpanded] = React.useState(false);
78+
79+
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
80+
setIsExpanded(isExpanded);
81+
};
6482

6583
const handleNewPage = (_evt: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => {
6684
setPage(newPage);
@@ -93,10 +111,23 @@ const SourcesCard: React.FunctionComponent<SourcesCardProps> = ({
93111
</Button>
94112
</CardTitle>
95113
{sources[page - 1].body && (
96-
<CardBody
97-
className={`pf-chatbot__sources-card-body ${sources.length === 1 && 'pf-chatbot__sources-card-no-footer'}`}
98-
>
99-
{sources[page - 1].body}
114+
<CardBody className={`pf-chatbot__sources-card-body`}>
115+
{sources[page - 1].hasShowMore ? (
116+
// prevents extra VO announcements of button text - parent Message has aria-live
117+
<div aria-live="off">
118+
<ExpandableSection
119+
variant={ExpandableSectionVariant.truncate}
120+
toggleText={isExpanded ? showLessWords : showMoreWords}
121+
onToggle={onToggle}
122+
isExpanded={isExpanded}
123+
truncateMaxLines={2}
124+
>
125+
{sources[page - 1].body}
126+
</ExpandableSection>
127+
</div>
128+
) : (
129+
<div className="pf-chatbot__sources-card-body-text">{sources[page - 1].body}</div>
130+
)}
100131
</CardBody>
101132
)}
102133
{sources.length > 1 && (
@@ -129,6 +160,9 @@ const SourcesCard: React.FunctionComponent<SourcesCardProps> = ({
129160
</svg>
130161
</Icon>
131162
</Button>
163+
<span aria-hidden="true">
164+
{page}/{sources.length}
165+
</span>
132166
<Button
133167
variant={ButtonVariant.plain}
134168
isDisabled={isDisabled || page === sources.length}
@@ -156,10 +190,6 @@ const SourcesCard: React.FunctionComponent<SourcesCardProps> = ({
156190
</Icon>
157191
</Button>
158192
</nav>
159-
160-
<span aria-hidden="true">
161-
{page} {ofWord} {sources.length}
162-
</span>
163193
</div>
164194
</CardFooter>
165195
)}

0 commit comments

Comments
 (0)