diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx index 2131b7f55..40175562d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx @@ -3,15 +3,29 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message'; import patternflyAvatar from './patternfly_avatar.jpg'; export const MessageWithDeepThinkingExample: FunctionComponent = () => ( - + <> + + + ); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx index 3498e3abb..ac9a9952a 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx @@ -31,7 +31,7 @@ export const MessageWithToolCallExample: FunctionComponent = () => { name="Bot" role="bot" avatar={patternflyAvatar} - content="This example has an expandable tool call title, with an additional description::" + content="This example has an expandable tool call title, with an additional description:" toolCall={{ titleText: "Calling 'awesome_tool_expansion'", expandableContent: 'This is the expandable content for the tool call.', @@ -39,6 +39,19 @@ export const MessageWithToolCallExample: FunctionComponent = () => { loadingText: "Loading 'awesome_tool_expansion'" }} /> + ); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx index 6ae583dd0..7303f9a14 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx @@ -23,113 +23,230 @@ export const MessageWithToolResponseExample: FunctionComponent = () => { }; return ( - - - - - - - - - toolName - - - - - Execution time: - 0.12 seconds - - - - - - - - - ), - cardBody: ( - <> - + - + + + + + + + toolName + + + + + Execution time: + 0.12 seconds + + + + + + + + + ), + cardBody: ( + <> + - Parameters - - - Optional description text for parameters. - - - - - - - - - - - - - - - - - - - - + Parameters + + + Optional description text for parameters. + + + + + + + + + + + + + + + + + + + + + Response + + + Descriptive text about the tool response, including completion status, details on the data that + was processed, or anything else relevant to the use case. + + + + + + ) + }} + /> + + + + + + + + + toolName + + + + + Execution time: + 0.12 seconds + + + + + + + + + ), + cardBody: ( + <> + - Response - - - Descriptive text about the tool response, including completion status, details on the data that was - processed, or anything else relevant to the use case. - - - - - - ) - }} - /> + + Parameters + + + Optional description text for parameters. + + + + + + + + + + + + + + + + + + + + + Response + + + Descriptive text about the tool response, including completion status, details on the data that + was processed, or anything else relevant to the use case. + + + + + + ) + }} + /> + ); }; diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx index 03f333385..1621e4e02 100644 --- a/packages/module/src/DeepThinking/DeepThinking.test.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import DeepThinking from './DeepThinking'; @@ -58,4 +59,64 @@ describe('DeepThinking', () => { const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading'); expect(subheadingContainer).toBeFalsy(); }); + + it('should pass through cardBodyProps', () => { + render( + + ); + + const cardBody = screen.getByText('Thinking content').closest('.pf-v6-c-card__body'); + expect(cardBody).toHaveClass('custom-card-body-class'); + }); + + it('Renders expanded by default', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('Thinking content')).toBeVisible(); + }); + + it('Renders collapsed when isDefaultExpanded is false', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByText('Thinking content')).not.toBeVisible(); + }); + + it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => { + render( + + ); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('Thinking content')).toBeVisible(); + }); + + it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => { + const user = userEvent.setup(); + const customOnToggle = jest.fn(); + + render( + + ); + + const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent }); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + await user.click(toggleButton); + + expect(customOnToggle).toHaveBeenCalled(); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByText('Thinking content')).not.toBeVisible(); + }); }); diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index 9b485cb6b..11c29549d 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -14,6 +14,8 @@ import { useState, type FunctionComponent } from 'react'; export interface DeepThinkingProps { /** Toggle content shown for expandable section */ toggleContent: React.ReactNode; + /** Flag indicating whether the expandable content is expanded by default. */ + isDefaultExpanded?: boolean; /** Additional props passed to expandable section */ expandableSectionProps?: Omit; /** Subheading rendered inside expandable section */ @@ -32,9 +34,10 @@ export const DeepThinking: FunctionComponent = ({ expandableSectionProps, subheading, toggleContent, + isDefaultExpanded = true, cardBodyProps }: DeepThinkingProps) => { - const [isExpanded, setIsExpanded] = useState(true); + const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { setIsExpanded(isExpanded); diff --git a/packages/module/src/ToolCall/ToolCall.test.tsx b/packages/module/src/ToolCall/ToolCall.test.tsx index a43c9d64c..91f4e7432 100644 --- a/packages/module/src/ToolCall/ToolCall.test.tsx +++ b/packages/module/src/ToolCall/ToolCall.test.tsx @@ -181,4 +181,55 @@ describe('ToolCall', () => { render(); expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible(); }); + + it('Renders collapsed by default when expandableContent is provided', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByText('Expandable Content')).not.toBeVisible(); + }); + + it('Renders expanded when isDefaultExpanded is true', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('Expandable Content')).toBeVisible(); + }); + + it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => { + render( + + ); + + expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('Expandable Content')).toBeVisible(); + }); + + it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => { + const user = userEvent.setup(); + const customOnToggle = jest.fn(); + + render( + + ); + + const toggleButton = screen.getByRole('button', { name: defaultProps.titleText }); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + await user.click(toggleButton); + + expect(customOnToggle).toHaveBeenCalledTimes(1); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByText('Expandable Content')).not.toBeVisible(); + }); }); diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index a3e6e67d8..774984813 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -1,4 +1,4 @@ -import { type FunctionComponent } from 'react'; +import { useState, type FunctionComponent } from 'react'; import { ActionList, ActionListProps, @@ -31,6 +31,8 @@ export interface ToolCallProps { spinnerProps?: SpinnerProps; /** Content to render within an expandable section. */ expandableContent?: React.ReactNode; + /** Flag indicating whether the expandable content is expanded by default. */ + isDefaultExpanded?: boolean; /** Text content for the "run" action button. */ runButtonText?: string; /** Additional props for the "run" action button. */ @@ -66,6 +68,7 @@ export const ToolCall: FunctionComponent = ({ loadingText, isLoading, expandableContent, + isDefaultExpanded = false, runButtonText = 'Run tool', runButtonProps, runActionItemProps, @@ -82,6 +85,12 @@ export const ToolCall: FunctionComponent = ({ expandableSectionProps, spinnerProps }: ToolCallProps) => { + const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); + + const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + const titleContent = ( {isLoading ? ( @@ -124,6 +133,8 @@ export const ToolCall: FunctionComponent = ({ diff --git a/packages/module/src/ToolResponse/ToolResponse.test.tsx b/packages/module/src/ToolResponse/ToolResponse.test.tsx index 91cdd6950..8d4e3135d 100644 --- a/packages/module/src/ToolResponse/ToolResponse.test.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import ToolResponse from './ToolResponse'; @@ -105,4 +106,47 @@ describe('ToolResponse', () => { const { container } = render(); expect(container.querySelector('.pf-v6-c-divider')).toBeFalsy(); }); + + it('Renders expanded by default', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText(defaultProps.cardTitle)).toBeVisible(); + expect(screen.getByText(defaultProps.cardBody)).toBeVisible(); + }); + + it('Renders collapsed when isDefaultExpanded is false', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible(); + expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible(); + }); + + it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => { + render(); + + expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText(defaultProps.cardTitle)).toBeVisible(); + expect(screen.getByText(defaultProps.cardBody)).toBeVisible(); + }); + + it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => { + const user = userEvent.setup(); + const customOnToggle = jest.fn(); + + render( + + ); + + const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent }); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + await user.click(toggleButton); + + expect(customOnToggle).toHaveBeenCalledTimes(1); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible(); + expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible(); + }); }); diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index d7dc7d608..39bfdb269 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -18,6 +18,8 @@ import { useState, type FunctionComponent } from 'react'; export interface ToolResponseProps { /** Toggle content shown for expandable section */ toggleContent: React.ReactNode; + /** Flag indicating whether the expandable content is expanded by default. */ + isDefaultExpanded?: boolean; /** Additional props passed to expandable section */ expandableSectionProps?: Omit; /** Subheading rendered inside expandable section */ @@ -51,12 +53,13 @@ export const ToolResponse: FunctionComponent = ({ cardTitle, cardBodyProps, toggleContent, + isDefaultExpanded = true, toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, toolResponseCardTitleProps }: ToolResponseProps) => { - const [isExpanded, setIsExpanded] = useState(true); + const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { setIsExpanded(isExpanded);