From b62fa26323a597e039df84e2c6f36c783264ba25 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Thu, 21 Aug 2025 11:38:21 -0400 Subject: [PATCH 1/2] feat(SourcesCard): Allow for subtitles and custom footers Added demo with tests. Also passing some additional props down while I am in there. --- .../examples/Messages/MessageWithSources.tsx | 70 +++++++ .../chatbot/examples/Messages/Messages.md | 2 +- .../module/src/SourcesCard/SourcesCard.scss | 16 ++ .../src/SourcesCard/SourcesCard.test.tsx | 93 +++++++++ .../module/src/SourcesCard/SourcesCard.tsx | 196 +++++++++++------- 5 files changed, 296 insertions(+), 81 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx index 060252ee6..5fb3f3873 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx @@ -1,6 +1,8 @@ import { FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react'; import Message from '@patternfly/chatbot/dist/dynamic/Message'; import patternflyAvatar from './patternfly_avatar.jpg'; +import { Button, Flex, FlexItem, Label, Popover } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; export const MessageWithSourcesExample: FunctionComponent = () => { const onSetPage = (_event: ReactMouseEvent | ReactKeyboardEvent | MouseEvent, newPage: number) => { @@ -8,8 +10,76 @@ export const MessageWithSourcesExample: FunctionComponent = () => { console.log(`Page changed to ${newPage}`); }; + const date = new Date(); + + const datePart = date.toLocaleDateString('en', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + + const timePart = date.toLocaleTimeString('en', { + hour: '2-digit', + minute: '2-digit', + hour12: true + }); + + const formattedDate = `${datePart}, ${timePart}`; + return ( <> + + + + + + + + + + + + Why this confidence score? + + } + bodyContent={ + <> + A high confidence score indicates a strong match. The system found significant overlap in + key data points, including text content, names, dates, and organizational details, with a + high degree of certainty. This match is highly reliable. + + } + > + + + + + + {`Last updated: ${formattedDate}`} + + ) + } + ] + }} + /> { ); expect(screen.getByRole('link', { name: /How to make an apple pie/i })).toHaveClass('test'); }); + + it('should apply cardTitleProps appropriately', () => { + render( + + ); + expect(screen.getByTestId('card-title')).toHaveClass('test'); + }); + + it('should apply cardBodyProps appropriately', () => { + render( + + ); + expect(screen.getByTestId('card-body')).toHaveClass('test'); + }); + + it('should apply cardFooterProps appropriately', () => { + render( + + ); + expect(screen.getByTestId('card-footer')).toHaveClass('test'); + }); + + it('should apply truncateProps appropriately', () => { + render( + + ); + expect(screen.getByTestId('card-truncate')).toHaveClass('test'); + }); + + it('should apply custom footer appropriately when there is one source', () => { + render( + I am a custom footer }]} /> + ); + expect(screen.getByText('I am a custom footer')); + expect(screen.queryByText('1/1')).toBeFalsy(); + }); + + it('should apply custom footer appropriately when are multiple sources', () => { + render( + I am a custom footer }, + { title: 'How to bake bread', link: '' } + ]} + /> + ); + expect(screen.getByText('I am a custom footer')); + // does not show navigation bar + expect(screen.queryByText('1/2')).toBeFalsy(); + }); + + it('should apply footer props to custom footer appropriately', () => { + render( + I am a custom footer }]} + /> + ); + expect(screen.getByText('I am a custom footer')); + expect(screen.getByTestId('card-footer')).toHaveClass('test'); + }); + + it('should apply subtitle appropriately', () => { + render( + + ); + expect(screen.getByText('How to make an apple pie')); + expect(screen.getByText('You must first create the universe')); + }); }); diff --git a/packages/module/src/SourcesCard/SourcesCard.tsx b/packages/module/src/SourcesCard/SourcesCard.tsx index c589ef5ba..446db1d28 100644 --- a/packages/module/src/SourcesCard/SourcesCard.tsx +++ b/packages/module/src/SourcesCard/SourcesCard.tsx @@ -10,14 +10,18 @@ import { ButtonVariant, Card, CardBody, + CardBodyProps, CardFooter, + CardFooterProps, CardProps, CardTitle, + CardTitleProps, ExpandableSection, ExpandableSectionVariant, Icon, pluralize, - Truncate + Truncate, + TruncateProps } from '@patternfly/react-core'; import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; @@ -34,6 +38,8 @@ export interface SourcesCardProps extends CardProps { sources: { /** Title of sources card */ title?: string; + /** Subtitle of sources card */ + subtitle?: string; /** Link to source */ link: string; /** Body of sources card */ @@ -46,6 +52,10 @@ export interface SourcesCardProps extends CardProps { onClick?: React.MouseEventHandler; /** Any additional props applied to the title of the Sources card */ titleProps?: ButtonProps; + /** Custom footer applied to the Sources card */ + footer?: React.ReactNode; + /** Additional props passed to Truncate component */ + truncateProps?: TruncateProps; }[]; /** Label for the English word "source" */ sourceWord?: string; @@ -65,6 +75,12 @@ export interface SourcesCardProps extends CardProps { showMoreWords?: string; /** Label for English words "show less" */ showLessWords?: string; + /** Additional props passed to card title */ + cardTitleProps?: CardTitleProps; + /** Additional props passed to card body */ + cardBodyProps?: CardBodyProps; + /** Additional props passed to card footer */ + cardFooterProps?: CardFooterProps; } const SourcesCard: FunctionComponent = ({ @@ -82,6 +98,9 @@ const SourcesCard: FunctionComponent = ({ showMoreWords = 'show more', showLessWords = 'show less', isCompact, + cardTitleProps, + cardBodyProps, + cardFooterProps, ...props }: SourcesCardProps) => { const [page, setPage] = useState(1); @@ -96,9 +115,9 @@ const SourcesCard: FunctionComponent = ({ onSetPage && onSetPage(_evt, newPage); }; - const renderTitle = (title?: string) => { + const renderTitle = (title?: string, truncateProps?: TruncateProps) => { if (title) { - return ; + return ; } return `Source ${page}`; }; @@ -107,24 +126,32 @@ const SourcesCard: FunctionComponent = ({
{pluralize(sources.length, sourceWord, sourceWordPlural)} - - + +
+ + {sources[page - 1].subtitle && ( + {sources[page - 1].subtitle} + )} +
{sources[page - 1].body && ( - + {sources[page - 1].hasShowMore ? ( // prevents extra VO announcements of button text - parent Message has aria-live
@@ -143,68 +170,77 @@ const SourcesCard: FunctionComponent = ({ )} )} - {sources.length > 1 && ( - -
- -
+ {sources[page - 1].footer ? ( + + {sources[page - 1].footer} + ) : ( + sources.length > 1 && ( + +
+ +
+
+ ) )}
From cfb146ad9cc02c94e9724e584a8f476604cb4723 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Thu, 4 Sep 2025 10:47:45 -0400 Subject: [PATCH 2/2] Update styles --- .../chatbot/examples/Messages/MessageWithSources.tsx | 4 ++-- packages/module/src/SourcesCard/SourcesCard.scss | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx index 5fb3f3873..b29962c00 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx @@ -42,7 +42,7 @@ export const MessageWithSourcesExample: FunctionComponent = () => { 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 ...', isExternal: true, footer: ( - + @@ -51,7 +51,7 @@ export const MessageWithSourcesExample: FunctionComponent = () => { + diff --git a/packages/module/src/SourcesCard/SourcesCard.scss b/packages/module/src/SourcesCard/SourcesCard.scss index 54b71f4e8..aa403dd08 100644 --- a/packages/module/src/SourcesCard/SourcesCard.scss +++ b/packages/module/src/SourcesCard/SourcesCard.scss @@ -17,7 +17,7 @@ } .pf-chatbot__compact-sources-card-body { - --pf-v6-c-card--child--PaddingBlockEnd: var(--pf-t--global--spacer--md); + --pf-v6-c-card--child--PaddingBlockEnd: var(--pf-t--global--spacer--xs); } .pf-chatbot__sources-card-subtitle, @@ -40,6 +40,7 @@ .pf-chatbot__sources-card-title-container { display: flex; + flex-direction: column; gap: var(--pf-t--global--spacer--xs); }