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..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,36 @@ export const MessageWithSourcesExample: React.FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with very 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: [ + { + title: 'Getting started with Red Hat OpenShift', + link: '#', + 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.', + hasShowMore: true + }, + { + title: 'Azure Red Hat OpenShift documentation', + link: '#', + body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure.', + hasShowMore: true + }, + { + title: 'OKD Documentation: Home', + link: '#', + 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.', + hasShowMore: true + } + ], + onSetPage + }} + /> + { name="Bot" role="bot" avatar={patternflyAvatar} - content="Example with only 1 source" + content="This example only includes 1 source:" sources={{ sources: [ { @@ -83,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 }, @@ -105,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: [ { 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" diff --git a/packages/module/src/SourcesCard/SourcesCard.scss b/packages/module/src/SourcesCard/SourcesCard.scss index 3e3a6f7bf..7a0ab1d65 100644 --- a/packages/module/src/SourcesCard/SourcesCard.scss +++ b/packages/module/src/SourcesCard/SourcesCard.scss @@ -16,7 +16,7 @@ box-shadow: var(--pf-t--global--box-shadow--sm); } -.pf-chatbot__sources-card-body { +.pf-chatbot__sources-card-body-text { display: block; display: -webkit-box; height: 2.8125rem; @@ -25,11 +25,6 @@ -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; - margin-bottom: var(--pf-t--global--spacer--md); -} - -.pf-chatbot__sources-card-no-footer { - margin-bottom: var(--pf-t--global--spacer--lg); } .pf-chatbot__sources-card-footer-container { @@ -38,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 4d02f8fb4..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( { 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('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 2d0956315..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,12 +25,18 @@ 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; /** 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,12 +51,15 @@ 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 = ({ className, isDisabled, - ofWord = 'of', paginationAriaLabel = 'Pagination', sources, sourceWord = 'source', @@ -58,9 +69,16 @@ const SourcesCard: React.FunctionComponent = ({ onNextClick, onPreviousClick, onSetPage, + showMoreWords = 'show more', + showLessWords = 'show less', ...props }: SourcesCardProps) => { const [page, setPage] = React.useState(1); + 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) => { setPage(newPage); @@ -93,10 +111,23 @@ const SourcesCard: React.FunctionComponent = ({ {sources[page - 1].body && ( - - {sources[page - 1].body} + + {sources[page - 1].hasShowMore ? ( + // prevents extra VO announcements of button text - parent Message has aria-live +
+ + {sources[page - 1].body} + +
+ ) : ( +
{sources[page - 1].body}
+ )}
)} {sources.length > 1 && ( @@ -129,6 +160,9 @@ const SourcesCard: React.FunctionComponent = ({ + - - )}