From 716e51d7356f4811f6120fcc0a62f0d37ca94541 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 28 Mar 2025 16:20:28 -0400 Subject: [PATCH 1/4] feat(SourcesCard): Add expanded view Allow for cards with more source info. --- .../examples/Messages/MessageWithSources.tsx | 29 ++++++++++++++ .../module/src/SourcesCard/SourcesCard.scss | 7 +--- .../src/SourcesCard/SourcesCard.test.tsx | 28 +++++++++++++ .../module/src/SourcesCard/SourcesCard.tsx | 39 ++++++++++++++++--- 4 files changed, 92 insertions(+), 11 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 47a64c71a..fe21e2eb3 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 @@ -39,6 +39,35 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { onSetPage }} /> + { await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i })); expect(spy).toHaveBeenCalledTimes(2); }); + + it('should handle showMore appropriately', async () => { + render( + + ); + expect(screen.getByRole('button', { name: /Show more/i })); + await userEvent.click(screen.getByRole('button', { name: /Show more/i })); + expect(screen.getByRole('button', { name: /Show less/i })); + }); }); diff --git a/packages/module/src/SourcesCard/SourcesCard.tsx b/packages/module/src/SourcesCard/SourcesCard.tsx index 2d0956315..099ba0c3c 100644 --- a/packages/module/src/SourcesCard/SourcesCard.tsx +++ b/packages/module/src/SourcesCard/SourcesCard.tsx @@ -28,7 +28,13 @@ export interface SourcesCardProps extends CardProps { /** Accessible label for the pagination component. */ paginationAriaLabel?: string; /** Content rendered inside the paginated card */ - sources: { title?: string; link: string; body?: React.ReactNode | string; isExternal?: boolean }[]; + sources: { + title?: string; + link: string; + body?: React.ReactNode | string; + isExternal?: boolean; + hasShowMore?: boolean; + }[]; /** Label for the English word "source" */ sourceWord?: string; /** Plural for sourceWord */ @@ -43,6 +49,10 @@ export interface SourcesCardProps extends CardProps { onPreviousClick?: (event: React.SyntheticEvent, page: number) => void; /** Function called when page is changed. */ onSetPage?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => void; + /** Label for English words "show more" */ + showMoreWords?: string; + /** Label for English words "show less" */ + showLessWords?: string; } const SourcesCard: React.FunctionComponent = ({ @@ -58,11 +68,15 @@ const SourcesCard: React.FunctionComponent = ({ onNextClick, onPreviousClick, onSetPage, + showMoreWords = 'show more', + showLessWords = 'show less', ...props }: SourcesCardProps) => { const [page, setPage] = React.useState(1); + const [showMore, setShowMore] = React.useState(false); const handleNewPage = (_evt: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => { + setShowMore(false); setPage(newPage); onSetPage && onSetPage(_evt, newPage); }; @@ -93,10 +107,25 @@ const SourcesCard: React.FunctionComponent = ({ {sources[page - 1].body && ( - - {sources[page - 1].body} + +
+ {sources[page - 1].body} +
+
+ {sources[page - 1].hasShowMore && ( + + )} +
)} {sources.length > 1 && ( From b5970ec266be887437f8a87f69bcc2b9ba4e75f5 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 7 Apr 2025 10:36:27 -0400 Subject: [PATCH 2/4] Update footer --- .../module/src/SourcesCard/SourcesCard.scss | 3 +- .../src/SourcesCard/SourcesCard.test.tsx | 25 ++------- .../module/src/SourcesCard/SourcesCard.tsx | 51 ++++++++++--------- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/packages/module/src/SourcesCard/SourcesCard.scss b/packages/module/src/SourcesCard/SourcesCard.scss index 749fc57d1..7a0ab1d65 100644 --- a/packages/module/src/SourcesCard/SourcesCard.scss +++ b/packages/module/src/SourcesCard/SourcesCard.scss @@ -33,13 +33,14 @@ var(--pf-t--global--spacer--sm) !important; .pf-chatbot__sources-card-footer { display: flex; - justify-content: space-between; align-items: center; &-buttons { display: flex; gap: var(--pf-t--global--spacer--xs); align-items: center; + justify-content: space-between; + flex: 1; .pf-v6-c-button { border-radius: var(--pf-t--global--border--radius--pill); diff --git a/packages/module/src/SourcesCard/SourcesCard.test.tsx b/packages/module/src/SourcesCard/SourcesCard.test.tsx index aa6a937bf..e75afb3ac 100644 --- a/packages/module/src/SourcesCard/SourcesCard.test.tsx +++ b/packages/module/src/SourcesCard/SourcesCard.test.tsx @@ -11,7 +11,7 @@ describe('SourcesCard', () => { expect(screen.getByText('Source 1')).toBeTruthy(); // no buttons or navigation when there is only 1 source expect(screen.queryByRole('button')).toBeFalsy(); - expect(screen.queryByText('1 of 1')).toBeFalsy(); + expect(screen.queryByText('1/1')).toBeFalsy(); }); it('should render card correctly if one source with a title is passed in', () => { @@ -48,7 +48,7 @@ describe('SourcesCard', () => { ); expect(screen.getByText('2 sources')).toBeTruthy(); expect(screen.getByText('How to make an apple pie')).toBeTruthy(); - expect(screen.getByText('1 of 2')).toBeTruthy(); + expect(screen.getByText('1/2')).toBeTruthy(); screen.getByRole('button', { name: /Go to previous page/i }); screen.getByRole('button', { name: /Go to next page/i }); }); @@ -63,12 +63,12 @@ describe('SourcesCard', () => { /> ); expect(screen.getByText('How to make an apple pie')).toBeTruthy(); - expect(screen.getByText('1 of 2')).toBeTruthy(); + expect(screen.getByText('1/2')).toBeTruthy(); expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled(); await userEvent.click(screen.getByRole('button', { name: /Go to next page/i })); expect(screen.queryByText('How to make an apple pie')).toBeFalsy(); expect(screen.getByText('How to make cookies')).toBeTruthy(); - expect(screen.getByText('2 of 2')).toBeTruthy(); + expect(screen.getByText('2/2')).toBeTruthy(); expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled(); expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled(); }); @@ -101,19 +101,6 @@ describe('SourcesCard', () => { expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled(); }); - it('should change ofWord appropriately', () => { - render( - - ); - expect(screen.getByText('1 de 2')).toBeTruthy(); - }); - it('should render navigation aria label appropriately', () => { render( { ]} /> ); - expect(screen.getByRole('button', { name: /Show more/i })); - await userEvent.click(screen.getByRole('button', { name: /Show more/i })); - expect(screen.getByRole('button', { name: /Show less/i })); + expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content'); }); }); diff --git a/packages/module/src/SourcesCard/SourcesCard.tsx b/packages/module/src/SourcesCard/SourcesCard.tsx index 099ba0c3c..3c4a85bd7 100644 --- a/packages/module/src/SourcesCard/SourcesCard.tsx +++ b/packages/module/src/SourcesCard/SourcesCard.tsx @@ -12,6 +12,8 @@ import { CardFooter, CardProps, CardTitle, + ExpandableSection, + ExpandableSectionVariant, Icon, pluralize, Truncate @@ -23,7 +25,7 @@ export interface SourcesCardProps extends CardProps { className?: string; /** Flag indicating if the pagination is disabled. */ isDisabled?: boolean; - /** Label for the English word "of". */ + /** @deprecated ofWord has been deprecated. Label for the English word "of." */ ofWord?: string; /** Accessible label for the pagination component. */ paginationAriaLabel?: string; @@ -58,7 +60,6 @@ export interface SourcesCardProps extends CardProps { const SourcesCard: React.FunctionComponent = ({ className, isDisabled, - ofWord = 'of', paginationAriaLabel = 'Pagination', sources, sourceWord = 'source', @@ -73,10 +74,13 @@ const SourcesCard: React.FunctionComponent = ({ ...props }: SourcesCardProps) => { const [page, setPage] = React.useState(1); - const [showMore, setShowMore] = React.useState(false); + const [isExpanded, setIsExpanded] = React.useState(false); + + const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; const handleNewPage = (_evt: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => { - setShowMore(false); setPage(newPage); onSetPage && onSetPage(_evt, newPage); }; @@ -108,24 +112,22 @@ const SourcesCard: React.FunctionComponent = ({ {sources[page - 1].body && ( -
- {sources[page - 1].body} -
-
- {sources[page - 1].hasShowMore && ( - - )} -
+ {sources[page - 1].body} + + + ) : ( +
{sources[page - 1].body}
+ )}
)} {sources.length > 1 && ( @@ -158,6 +160,9 @@ const SourcesCard: React.FunctionComponent = ({ + - - )} From e8bb8af8fd2746c10c00e6c816ac45c23dc7a2a2 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 8 Apr 2025 09:40:25 -0400 Subject: [PATCH 3/4] Address docs feedback --- .../content/extensions/chatbot/examples/Messages/Messages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 1d7f953fd..7a86d9be9 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -144,7 +144,7 @@ If you are using Retrieval-Augmented Generation, you may want to display sources If a source will open outside of the ChatBot window, add an external link icon via `isExternal`. -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. +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. ```js file="./MessageWithSources.tsx" From a0d928cb36fca753258f43c3d0098da9fbb0fe4a Mon Sep 17 00:00:00 2001 From: Erin Donehoo Date: Wed, 9 Apr 2025 17:18:06 -0400 Subject: [PATCH 4/4] Refines example content for pr 501. --- .../chatbot/examples/Messages/MessageWithSources.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 fe21e2eb3..e3ba6119f 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 @@ -14,7 +14,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with sources" + content="This example has a body description that's within the recommended limit of 2 lines:" sources={{ sources: [ { @@ -43,7 +43,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with long sources" + 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:" sources={{ sources: [ { @@ -72,7 +72,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with very long sources" + content="This example has a truncated title:" sources={{ sources: [ { @@ -95,7 +95,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with only 1 source" + content="This example only includes 1 source:" sources={{ sources: [ { @@ -112,7 +112,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with sources that include a title and link" + content="This example has a title and no body description:" sources={{ sources: [ { title: 'Getting started with Red Hat OpenShift', link: '#', isExternal: true }, @@ -134,7 +134,7 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with link-only sources (not recommended)" + content="This example displays the source as a link, without a title (not recommended)" sources={{ sources: [ {