Skip to content

Commit 15a3d1e

Browse files
authored
Merge pull request #802 from thatblindgeye/iss657_multiSourceLayout
feat(Sources): added non-paginated layout
2 parents a680325 + e55d09c commit 15a3d1e

File tree

6 files changed

+200
-7
lines changed

6 files changed

+200
-7
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,43 @@ export const MessageWithSourcesExample: FunctionComponent = () => {
241241
}}
242242
isCompact
243243
/>
244+
245+
<Message
246+
name="Bot"
247+
role="bot"
248+
avatar={patternflyAvatar}
249+
content="This example demonstrates the non-paginated layout option. When enabled, all source cards are displayed in a flex layout that wraps automatically based on available space:"
250+
sources={{
251+
sources: [
252+
{
253+
title: 'Getting started with Red Hat OpenShift',
254+
link: '#',
255+
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.',
256+
isExternal: true,
257+
hasShowMore: true
258+
},
259+
{
260+
title: 'Azure Red Hat OpenShift documentation',
261+
link: '#',
262+
body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure.',
263+
isExternal: true
264+
},
265+
{
266+
title: 'OKD Documentation: Home',
267+
link: '#',
268+
body: 'OKD is a distribution of Kubernetes optimized for continuous application development and multi-tenant deployment.',
269+
isExternal: true
270+
},
271+
{
272+
title: 'Red Hat OpenShift Container Platform',
273+
link: '#',
274+
body: 'Red Hat OpenShift Container Platform is a Kubernetes platform that provides a cloud-like experience anywhere it is deployed.',
275+
isExternal: true
276+
}
277+
],
278+
layout: 'wrap'
279+
}}
280+
/>
244281
</>
245282
);
246283
};

packages/module/src/SourcesCard/SourcesCard.scss

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
flex-direction: column;
55
gap: var(--pf-t--global--spacer--sm);
66
padding-block-start: var(--pf-t--global--spacer--sm);
7-
max-width: 22.5rem;
7+
8+
&:not(.pf-m-wrap) {
9+
max-width: 22.5rem;
10+
}
811
}
912

1013
.pf-chatbot__sources-card-base {
@@ -15,6 +18,15 @@
1518
}
1619
}
1720

21+
.pf-chatbot__sources-card-base.pf-m-wrap {
22+
.pf-chatbot__sources-list {
23+
display: flex;
24+
flex-wrap: wrap;
25+
list-style: none;
26+
gap: var(--pf-t--global--spacer--xs);
27+
}
28+
}
29+
1830
.pf-chatbot__sources-card {
1931
box-shadow: var(--pf-t--global--box-shadow--sm);
2032
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,24 @@ describe('SourcesCard', () => {
2424
screen.getByRole('button', { name: /Go to previous page/i });
2525
screen.getByRole('button', { name: /Go to next page/i });
2626
});
27+
28+
it('should render with wrap layout when layout is set to wrap', () => {
29+
render(
30+
<SourcesCard
31+
layout="wrap"
32+
sources={[
33+
{ title: 'How to make an apple pie', link: '' },
34+
{ title: 'How to make cookies', link: '' },
35+
{ title: 'How to make a sandwich', link: '' }
36+
]}
37+
/>
38+
);
39+
40+
expect(screen.getByText('How to make an apple pie')).toBeVisible();
41+
expect(screen.getByText('How to make cookies')).toBeVisible();
42+
expect(screen.getByText('How to make a sandwich')).toBeVisible();
43+
44+
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
45+
expect(screen.queryByText('1/3')).not.toBeInTheDocument();
46+
});
2747
});

packages/module/src/SourcesCard/SourcesCard.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Chatbot Main - Messages - Sources Card
33
// ============================================================================
44
import type { FunctionComponent } from 'react';
5-
// Import PatternFly components
65
import {
76
ButtonProps,
87
CardBodyProps,
@@ -12,11 +11,16 @@ import {
1211
pluralize,
1312
TruncateProps
1413
} from '@patternfly/react-core';
14+
import { css } from '@patternfly/react-styles';
1515
import SourcesCardBase from '../SourcesCardBase';
1616

1717
export interface SourcesCardProps extends CardProps {
1818
/** Additional classes for the pagination navigation container. */
1919
className?: string;
20+
/** The layout used to display source cards. Use wrap to display and wrap all sources at once. */
21+
layout?: 'paginated' | 'wrap';
22+
/** Max width of a source card when the wrap layout is used. Can be any valid CSS width value. */
23+
cardMaxWidth?: string;
2024
/** Flag indicating if the pagination is disabled. */
2125
isDisabled?: boolean;
2226
/** @deprecated ofWord has been deprecated. Label for the English word "of." */
@@ -76,11 +80,13 @@ const SourcesCard: FunctionComponent<SourcesCardProps> = ({
7680
sources,
7781
sourceWord = 'source',
7882
sourceWordPlural = 'sources',
83+
layout = 'paginated',
84+
cardMaxWidth = '400px',
7985
...props
8086
}: SourcesCardProps) => (
81-
<div className="pf-chatbot__source">
87+
<div className={css('pf-chatbot__source', layout === 'wrap' && 'pf-m-wrap')}>
8288
<span>{pluralize(sources.length, sourceWord, sourceWordPlural)}</span>
83-
<SourcesCardBase sources={sources} {...props} />
89+
<SourcesCardBase sources={sources} layout={layout} cardMaxWidth={cardMaxWidth} {...props} />
8490
</div>
8591
);
8692

packages/module/src/SourcesCardBase/SourcesCardBase.test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,53 @@ describe('SourcesCardBase', () => {
233233
);
234234
expect(screen.getByRole('link', { name: /How to make an apple pie/i })).toHaveClass('test');
235235
});
236+
237+
it('should render with wrap layout when layout prop is set to wrap', () => {
238+
render(
239+
<SourcesCardBase
240+
layout="wrap"
241+
sources={[
242+
{ title: 'How to make an apple pie', link: '' },
243+
{ title: 'How to make cookies', link: '' },
244+
{ title: 'How to make a sandwich', link: '' }
245+
]}
246+
/>
247+
);
248+
249+
expect(screen.getByText('How to make an apple pie')).toBeVisible();
250+
expect(screen.getByText('How to make cookies')).toBeVisible();
251+
expect(screen.getByText('How to make a sandwich')).toBeVisible();
252+
253+
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
254+
expect(screen.queryByText('1/3')).not.toBeInTheDocument();
255+
});
256+
257+
it('should apply default cardMaxWidth when using wrap layout', () => {
258+
render(
259+
<SourcesCardBase
260+
layout="wrap"
261+
sources={[
262+
{ title: 'How to make an apple pie', link: '' },
263+
{ title: 'How to make cookies', link: '' }
264+
]}
265+
/>
266+
);
267+
const firstCard = screen.getByText('How to make an apple pie').closest('.pf-chatbot__sources-card');
268+
expect(firstCard).toHaveStyle({ maxWidth: '400px' });
269+
});
270+
271+
it('should apply custom cardMaxWidth when using wrap layout', () => {
272+
render(
273+
<SourcesCardBase
274+
layout="wrap"
275+
sources={[
276+
{ title: 'How to make an apple pie', link: '' },
277+
{ title: 'How to make cookies', link: '' }
278+
]}
279+
cardMaxWidth="500px"
280+
/>
281+
);
282+
const firstCard = screen.getByText('How to make an apple pie').closest('.pf-chatbot__sources-card');
283+
expect(firstCard).toHaveStyle({ maxWidth: '500px' });
284+
});
236285
});

packages/module/src/SourcesCardBase/SourcesCardBase.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
2727
export interface SourcesCardBaseProps extends CardProps {
2828
/** Additional classes for the pagination navigation container. */
2929
className?: string;
30+
/** The layout used to display source cards. Use wrap to display and wrap all sources at once. */
31+
layout?: 'paginated' | 'wrap';
3032
/** Flag indicating if the pagination is disabled. */
3133
isDisabled?: boolean;
3234
/** @deprecated ofWord has been deprecated. Label for the English word "of." */
3335
ofWord?: string;
3436
/** Accessible label for the pagination component. */
3537
paginationAriaLabel?: string;
38+
/** Max width of a source card when the wrap layout is used. Can be any valid CSS width value. */
39+
cardMaxWidth?: string;
3640
/** Content rendered inside the paginated card */
3741
sources: {
3842
/** Title of sources card */
@@ -94,6 +98,8 @@ const SourcesCardBase: FunctionComponent<SourcesCardBaseProps> = ({
9498
cardTitleProps,
9599
cardBodyProps,
96100
cardFooterProps,
101+
layout = 'paginated',
102+
cardMaxWidth = '400px',
97103
...props
98104
}: SourcesCardBaseProps) => {
99105
const [page, setPage] = useState(1);
@@ -108,13 +114,76 @@ const SourcesCardBase: FunctionComponent<SourcesCardBaseProps> = ({
108114
onSetPage && onSetPage(_evt, newPage);
109115
};
110116

111-
const renderTitle = (title?: string, truncateProps?: TruncateProps) => {
117+
const renderTitle = (title?: string, index?: number, truncateProps?: TruncateProps) => {
112118
if (title) {
113119
return <Truncate content={title} {...truncateProps} />;
114120
}
115-
return `Source ${page}`;
121+
return `Source ${index !== undefined ? index + 1 : page}`;
116122
};
117123

124+
const renderUncontrolledSourceCard = (source: SourcesCardBaseProps['sources'][0], index: number) => (
125+
<li key={index} className="pf-chatbot__sources-list-item">
126+
<Card isCompact={isCompact} className="pf-chatbot__sources-card" style={{ maxWidth: cardMaxWidth }} {...props}>
127+
<CardTitle className="pf-chatbot__sources-card-title" {...cardTitleProps}>
128+
<div className="pf-chatbot__sources-card-title-container">
129+
<Button
130+
component="a"
131+
variant={ButtonVariant.link}
132+
href={source.link}
133+
icon={source.isExternal ? <ExternalLinkSquareAltIcon /> : undefined}
134+
iconPosition="end"
135+
isInline
136+
rel={source.isExternal ? 'noreferrer' : undefined}
137+
target={source.isExternal ? '_blank' : undefined}
138+
onClick={source.onClick ?? undefined}
139+
{...source.titleProps}
140+
>
141+
{renderTitle(source.title, index, source.truncateProps)}
142+
</Button>
143+
{source.subtitle && <span className="pf-chatbot__sources-card-subtitle">{source.subtitle}</span>}
144+
</div>
145+
</CardTitle>
146+
{source.body && (
147+
<CardBody
148+
className={`pf-chatbot__sources-card-body ${source.footer ? 'pf-chatbot__compact-sources-card-body' : undefined}`}
149+
{...cardBodyProps}
150+
>
151+
{source.hasShowMore ? (
152+
// prevents extra VO announcements of button text - parent Message has aria-live
153+
<div aria-live="off">
154+
<ExpandableSection
155+
variant={ExpandableSectionVariant.truncate}
156+
toggleTextCollapsed={showMoreWords}
157+
toggleTextExpanded={showLessWords}
158+
truncateMaxLines={2}
159+
>
160+
{source.body}
161+
</ExpandableSection>
162+
</div>
163+
) : (
164+
<div className="pf-chatbot__sources-card-body-text">{source.body}</div>
165+
)}
166+
</CardBody>
167+
)}
168+
{source.footer && (
169+
<CardFooter className="pf-chatbot__sources-card-footer" {...cardFooterProps}>
170+
{source.footer}
171+
</CardFooter>
172+
)}
173+
</Card>
174+
</li>
175+
);
176+
177+
if (layout === 'wrap') {
178+
return (
179+
<div className="pf-chatbot__sources-card-base pf-m-wrap">
180+
<ul className="pf-chatbot__sources-list" role="list">
181+
{sources.map((source, index) => renderUncontrolledSourceCard(source, index))}
182+
</ul>
183+
</div>
184+
);
185+
}
186+
118187
return (
119188
<div className="pf-chatbot__sources-card-base">
120189
<Card isCompact={isCompact} className="pf-chatbot__sources-card" {...props}>
@@ -132,7 +201,7 @@ const SourcesCardBase: FunctionComponent<SourcesCardBaseProps> = ({
132201
onClick={sources[page - 1].onClick ?? undefined}
133202
{...sources[page - 1].titleProps}
134203
>
135-
{renderTitle(sources[page - 1].title, sources[page - 1].truncateProps)}
204+
{renderTitle(sources[page - 1].title, undefined, sources[page - 1].truncateProps)}
136205
</Button>
137206
{sources[page - 1].subtitle && (
138207
<span className="pf-chatbot__sources-card-subtitle">{sources[page - 1].subtitle}</span>

0 commit comments

Comments
 (0)