From a7d30f32254f9538e28bb533f29f52d6e716e650 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Fri, 12 Jun 2026 15:45:57 -0400 Subject: [PATCH] feat(app): add expand-to-read for long requirement descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long framework requirement descriptions (e.g. NIST SP800-53 PL-2) were truncated to a single line in the customer app's Requirements view with only a clipped native tooltip — no way to read the full text. The framework editor already has expand arrows; this brings the same affordance (read-only) to the app. Adds a shared ExpandableDescription cell: truncated inline text plus a hover maximize button that opens a read-only dialog with the full description. Used by both the grouped and flat requirements tables. Full text is already on the client, so no new fetch. Co-Authored-By: Claude Opus 4.8 --- .../components/ExpandableDescription.test.tsx | 41 +++++++++++ .../components/ExpandableDescription.tsx | 69 +++++++++++++++++++ .../components/FrameworkRequirements.tsx | 9 ++- .../components/GroupedRequirementRow.tsx | 9 ++- 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.test.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.tsx diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.test.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.test.tsx new file mode 100644 index 0000000000..f567857b62 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.test.tsx @@ -0,0 +1,41 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ExpandableDescription } from './ExpandableDescription'; + +const LONG = + 'Develop security and privacy plans for the system that are consistent with the enterprise architecture.'; + +describe('ExpandableDescription', () => { + it('renders the description inline with a read-more affordance', () => { + render(); + expect(screen.getByText(LONG)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /read full description/i }), + ).toBeInTheDocument(); + }); + + it('opens a dialog with the full description and an identifier · name heading', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /read full description/i })); + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getByText('PL-2 · System Security')).toBeInTheDocument(); + expect(within(dialog).getByText(LONG)).toBeInTheDocument(); + }); + + it('renders an em dash and no button when there is no description', () => { + render(); + expect(screen.getByText('—')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /read full description/i })).toBeNull(); + }); + + it('does not trigger the clickable parent row when expanding', () => { + const onRowClick = vi.fn(); + render( +
+ +
, + ); + fireEvent.click(screen.getByRole('button', { name: /read full description/i })); + expect(onRowClick).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.tsx new file mode 100644 index 0000000000..57f5a67dee --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/ExpandableDescription.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@trycompai/design-system'; +import { Maximize } from '@trycompai/design-system/icons'; +import { useState } from 'react'; + +interface ExpandableDescriptionProps { + description: string | null | undefined; + identifier?: string | null; + name?: string | null; +} + +/** + * Read-only requirement description cell. Shows the truncated text inline and, + * on hover, a maximize button that opens a dialog with the full description — + * long framework requirements (e.g. NIST SP800-53 PL-2) are otherwise + * unreadable behind the single-line truncation + native tooltip. + */ +export function ExpandableDescription({ + description, + identifier, + name, +}: ExpandableDescriptionProps) { + const [isOpen, setIsOpen] = useState(false); + + if (!description) { + return ; + } + + const heading = [identifier?.trim(), name].filter(Boolean).join(' · ') || 'Requirement'; + + return ( +
+ + {description} + + + + + + + {heading} + +
+ {description} +
+
+
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index 04bdbb62ab..1399b4e70b 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -26,6 +26,7 @@ import { import { Search } from '@trycompai/design-system/icons'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; +import { ExpandableDescription } from './ExpandableDescription'; import { REQUIREMENTS_TABLE_COLUMN_COUNT, REQUIREMENTS_TABLE_STYLE, @@ -215,9 +216,11 @@ export function FrameworkRequirements({ - - {item.description || '—'} - +
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/GroupedRequirementRow.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/GroupedRequirementRow.tsx index 0e25a56c64..f6cc83fa7c 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/GroupedRequirementRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/GroupedRequirementRow.tsx @@ -4,6 +4,7 @@ import { getRequirementStatus } from '@/lib/control-compliance'; import { Badge, TableCell, TableRow, Text } from '@trycompai/design-system'; import { Launch } from '@trycompai/design-system/icons'; import Link from 'next/link'; +import { ExpandableDescription } from './ExpandableDescription'; import type { RequirementItem } from './framework-controls-shared'; export function GroupedRequirementRow({ @@ -42,9 +43,11 @@ export function GroupedRequirementRow({ - - {item.description || '—'} - +