diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/McpSection.spec.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/McpSection.spec.tsx index 4564c1701f..c3cf943320 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/McpSection.spec.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/McpSection.spec.tsx @@ -1,5 +1,6 @@ import '@testing-library/jest-dom/extend-expect'; import { fireEvent, render, screen } from '@testing-library/react'; +import type { ReactNode } from 'react'; import type { McpItem } from '../../items/types'; import McpSection from '../sections/McpSection'; @@ -16,6 +17,11 @@ jest.mock('~/Providers', () => ({ useAgentPanelContext: () => ({ mcpServersMap: new Map() }), })); +jest.mock('~/components/ui', () => ({ + Collapse: ({ open, children }: { open: boolean; children: ReactNode }) => + open ? children : null, +})); + jest.mock('~/hooks', () => ({ useLocalize: () => (key: string) => key, useCopyToClipboard: () => jest.fn(), diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx index b67f8de1c4..81d48462b2 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx @@ -18,6 +18,7 @@ import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; import McpOAuthDialog from '~/components/MCP/McpOAuthDialog'; import { useAgentPanelContext } from '~/Providers'; import MCPToolItem from '../../../MCPToolItem'; +import { Collapse } from '~/components/ui'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; @@ -208,7 +209,7 @@ export default function McpSection({ item }: Props) {
-
+
{localize('com_ui_tools_mcp_tools_section')} @@ -299,39 +300,44 @@ export default function McpSection({ item }: Props) {
)}
- {hasTools && ( -
- {tools.map((tool) => ( - toggleToolSelect(tool.tool_id)} - onToggleDefer={() => toggleToolDefer(tool.tool_id)} - onToggleProgrammatic={() => toggleToolProgrammatic(tool.tool_id)} - /> - ))} -
- )} - {!hasTools && toolsLoading && ( -
- {['w-3/5', 'w-1/2', 'w-2/5'].map((width) => ( -
- - -
- ))} -
- )} - {!hasTools && !toolsLoading && ( -

- {localize('com_ui_tools_mcp_no_tools')} -

- )} + {/* Loading skeleton, tool list, and empty state share one slot and + * cross-swap via stacked collapses so their differing heights morph + * smoothly; the dialog's auto-height follows in a single motion. */} +
+ +
+ {tools.map((tool) => ( + toggleToolSelect(tool.tool_id)} + onToggleDefer={() => toggleToolDefer(tool.tool_id)} + onToggleProgrammatic={() => toggleToolProgrammatic(tool.tool_id)} + /> + ))} +
+
+ +
+ {['w-3/5', 'w-1/2', 'w-2/5'].map((width) => ( +
+ + +
+ ))} +
+
+ +

+ {localize('com_ui_tools_mcp_no_tools')} +

+
+
{configDialogProps && } diff --git a/client/src/components/ui/Collapse.tsx b/client/src/components/ui/Collapse.tsx new file mode 100644 index 0000000000..bffa300389 --- /dev/null +++ b/client/src/components/ui/Collapse.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react'; +import { cn } from '~/utils'; + +interface CollapseProps { + open: boolean; + children: ReactNode; + className?: string; +} + +/** + * Auto-height reveal via grid-template-rows 0fr -> 1fr, matching MCPToolItem and + * the OAuth QR. The parent's content-driven height follows the tween in a single + * motion, so stacked collapses (e.g. a loading skeleton swapping to a list) can + * cross-fade smoothly without a measuring wrapper fighting nested reveals. + * Content fades to soften the swap and is hidden from assistive tech while closed. + */ +export default function Collapse({ open, children, className }: CollapseProps) { + return ( +
+
+
+ {children} +
+
+
+ ); +} diff --git a/client/src/components/ui/__tests__/Collapse.spec.tsx b/client/src/components/ui/__tests__/Collapse.spec.tsx new file mode 100644 index 0000000000..59144e6048 --- /dev/null +++ b/client/src/components/ui/__tests__/Collapse.spec.tsx @@ -0,0 +1,28 @@ +import '@testing-library/jest-dom/extend-expect'; +import { render, screen } from '@testing-library/react'; +import Collapse from '../Collapse'; + +describe('Collapse', () => { + test('reveals its content when open', () => { + const { container } = render( + + + , + ); + expect(screen.getByTestId('child')).toBeInTheDocument(); + const root = container.firstChild as HTMLElement; + expect(root).toHaveClass('grid-rows-[1fr]'); + expect(root).not.toHaveAttribute('aria-hidden'); + }); + + test('collapses and hides from assistive tech when closed', () => { + const { container } = render( + + + , + ); + const root = container.firstChild as HTMLElement; + expect(root).toHaveClass('grid-rows-[0fr]'); + expect(root).toHaveAttribute('aria-hidden', 'true'); + }); +}); diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index 1412cf4aff..295a1921a0 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -1,4 +1,5 @@ export { Button } from '@librechat/client'; +export { default as Collapse } from './Collapse'; export { default as TermsAndConditionsModal } from './TermsAndConditionsModal'; export { default as AdminSettingsDialog } from './AdminSettingsDialog'; export type { PermissionConfig, AdminSettingsDialogProps } from './AdminSettingsDialog';