Skip to content

Commit 852c3be

Browse files
committed
feat(Sources): added non-paginated layout
1 parent 3549144 commit 852c3be

6 files changed

Lines changed: 298 additions & 9 deletions

File tree

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
1+
import { FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, useState } from 'react';
22
import Message from '@patternfly/chatbot/dist/dynamic/Message';
33
import patternflyAvatar from './patternfly_avatar.jpg';
4-
import { Button, Flex, FlexItem, Label, Popover } from '@patternfly/react-core';
4+
import { Button, Checkbox, Flex, FlexItem, Label, Popover } from '@patternfly/react-core';
55
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
66

77
export const MessageWithSourcesExample: FunctionComponent = () => {
@@ -241,6 +241,42 @@ 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+
},
258+
{
259+
title: 'Azure Red Hat OpenShift documentation',
260+
link: '#',
261+
body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...',
262+
isExternal: true
263+
},
264+
{
265+
title: 'OKD Documentation: Home',
266+
link: '#',
267+
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 ...',
268+
isExternal: true
269+
},
270+
{
271+
title: 'Red Hat OpenShift Container Platform',
272+
link: '#',
273+
body: 'Red Hat OpenShift Container Platform is a Kubernetes platform that provides a cloud-like experience anywhere it is deployed ...',
274+
isExternal: true
275+
}
276+
],
277+
layout: 'wrap'
278+
}}
279+
/>
244280
</>
245281
);
246282
};

packages/module/src/SourcesCard/SourcesCard.scss

Lines changed: 12 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,14 @@
1518
}
1619
}
1720

21+
.pf-chatbot__sources-card-base.pf-m-wrap {
22+
.pf-v6-c-list {
23+
display: flex;
24+
flex-wrap: wrap;
25+
gap: var(--pf-t--global--spacer--xs);
26+
}
27+
}
28+
1829
.pf-chatbot__sources-card {
1930
box-shadow: var(--pf-t--global--box-shadow--sm);
2031
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,56 @@ 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+
});
47+
48+
it('should pass listProps to SourcesCardBase when using wrap layout', () => {
49+
render(
50+
<SourcesCard
51+
layout="wrap"
52+
sources={[
53+
{ title: 'How to make an apple pie', link: '' },
54+
{ title: 'How to make cookies', link: '' }
55+
]}
56+
listProps={{ className: 'custom-list-class' }}
57+
/>
58+
);
59+
const listElement = screen.getByRole('list');
60+
expect(listElement).toHaveClass('custom-list-class');
61+
});
62+
63+
it('should pass listItemProps to SourcesCardBase when using wrap layout', () => {
64+
render(
65+
<SourcesCard
66+
layout="wrap"
67+
sources={[
68+
{ title: 'How to make an apple pie', link: '' },
69+
{ title: 'How to make cookies', link: '' }
70+
]}
71+
listItemProps={{ className: 'custom-list-item-class' }}
72+
/>
73+
);
74+
const listItemElements = screen.getAllByRole('listitem');
75+
listItemElements.forEach((listItem) => {
76+
expect(listItem).toHaveClass('custom-list-item-class');
77+
});
78+
});
2779
});

packages/module/src/SourcesCard/SourcesCard.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,31 @@
22
// Chatbot Main - Messages - Sources Card
33
// ============================================================================
44
import type { FunctionComponent } from 'react';
5-
// Import PatternFly components
65
import {
76
ButtonProps,
87
CardBodyProps,
98
CardFooterProps,
109
CardProps,
1110
CardTitleProps,
11+
ListProps,
12+
ListItemProps,
1213
pluralize,
1314
TruncateProps
1415
} from '@patternfly/react-core';
16+
import { css } from '@patternfly/react-styles';
1517
import SourcesCardBase from '../SourcesCardBase';
1618

1719
export interface SourcesCardProps extends CardProps {
1820
/** Additional classes for the pagination navigation container. */
1921
className?: string;
22+
/** The layout used to display source cards. Use wrap to display and wrap all sources at once. */
23+
layout?: 'paginated' | 'wrap';
24+
/** Max width of a source card when the wrap layout is used. Can be any valid CSS width value. */
25+
cardMaxWidth?: string;
26+
/** Additional props to pass to the list of source cards when the wrap layout is used. */
27+
listProps?: ListProps;
28+
/** Additional props to pass to the list items of source cards when the wrap layout is used. */
29+
listItemProps?: Omit<ListItemProps, 'children'>;
2030
/** Flag indicating if the pagination is disabled. */
2131
isDisabled?: boolean;
2232
/** @deprecated ofWord has been deprecated. Label for the English word "of." */
@@ -76,11 +86,15 @@ const SourcesCard: FunctionComponent<SourcesCardProps> = ({
7686
sources,
7787
sourceWord = 'source',
7888
sourceWordPlural = 'sources',
89+
layout = 'paginated',
90+
cardMaxWidth = '400px',
91+
listProps,
92+
listItemProps,
7993
...props
8094
}: SourcesCardProps) => (
81-
<div className="pf-chatbot__source">
95+
<div className={css('pf-chatbot__source', layout === 'wrap' && 'pf-m-wrap')}>
8296
<span>{pluralize(sources.length, sourceWord, sourceWordPlural)}</span>
83-
<SourcesCardBase sources={sources} {...props} />
97+
<SourcesCardBase sources={sources} layout={layout} listProps={listProps} listItemProps={listItemProps} {...props} />
8498
</div>
8599
);
86100

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,85 @@ 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+
});
285+
286+
it('should apply listProps when using wrap layout', () => {
287+
render(
288+
<SourcesCardBase
289+
layout="wrap"
290+
sources={[
291+
{ title: 'How to make an apple pie', link: '' },
292+
{ title: 'How to make cookies', link: '' }
293+
]}
294+
listProps={{ className: 'custom-list-class' }}
295+
/>
296+
);
297+
const listElement = screen.getByRole('list');
298+
expect(listElement).toHaveClass('custom-list-class');
299+
});
300+
301+
it('should apply listItemProps when using wrap layout', () => {
302+
render(
303+
<SourcesCardBase
304+
layout="wrap"
305+
sources={[
306+
{ title: 'How to make an apple pie', link: '' },
307+
{ title: 'How to make cookies', link: '' }
308+
]}
309+
listItemProps={{ className: 'custom-list-item-class' }}
310+
/>
311+
);
312+
const listItemElements = screen.getAllByRole('listitem');
313+
listItemElements.forEach((listItem) => {
314+
expect(listItem).toHaveClass('custom-list-item-class');
315+
});
316+
});
236317
});

0 commit comments

Comments
 (0)