Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(<ExpandableDescription description={LONG} identifier="PL-2" name="System Security" />);
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(<ExpandableDescription description={LONG} identifier="PL-2" name="System Security" />);
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(<ExpandableDescription description={null} identifier="PL-2" name="System Security" />);
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(
<div onClick={onRowClick}>
<ExpandableDescription description={LONG} identifier="PL-2" name="System Security" />
</div>,
);
fireEvent.click(screen.getByRole('button', { name: /read full description/i }));
expect(onRowClick).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -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 <span className="block truncate text-sm">—</span>;
}

const heading = [identifier?.trim(), name].filter(Boolean).join(' · ') || 'Requirement';

return (
<div className="group relative flex items-center">
<span className="block truncate pr-6 text-sm" title={description}>
{description}
</span>
<button
type="button"
aria-label="Read full description"
title="Read full description"
className="text-muted-foreground hover:text-foreground absolute right-0 top-1/2 -translate-y-1/2 rounded-xs p-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
onClick={(e) => {
// The row is a navigation target — don't follow it when expanding.
e.stopPropagation();
e.preventDefault();
setIsOpen(true);
}}
>
<Maximize size={14} />
</button>

<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent size="3xl">
<DialogHeader>
<DialogTitle>{heading}</DialogTitle>
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto whitespace-pre-wrap text-sm leading-relaxed">
{description}
</div>
</DialogContent>
</Dialog>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -215,9 +216,11 @@ export function FrameworkRequirements({
</span>
</TableCell>
<TableCell>
<span className="block truncate text-sm" title={item.description || ''}>
{item.description || '—'}
</span>
<ExpandableDescription
description={item.description}
identifier={item.identifier}
name={item.name}
/>
</TableCell>
<TableCell>
<div className="flex min-w-0 items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -42,9 +43,11 @@ export function GroupedRequirementRow({
</span>
</TableCell>
<TableCell>
<span className="block truncate text-sm" title={item.description || ''}>
{item.description || '—'}
</span>
<ExpandableDescription
description={item.description}
identifier={item.identifier}
name={item.name}
/>
</TableCell>
<TableCell>
<div className="flex min-w-0 items-center gap-2">
Expand Down
Loading