feat(FN-678): add title truncation and tooltip to TaskCard

- Truncate long board titles in TaskCard with CSS text truncation\n- Add tooltip showing full title on hover via title attribute\n- Update TaskCard tests for truncation behavior\n- Add changeset for board title truncation fix
This commit is contained in:
gsxdsm
2026-04-01 07:16:54 -07:00
parent 4750d75b45
commit 228648adfa
3 changed files with 105 additions and 10 deletions

View File

@@ -2,6 +2,4 @@
"@gsxdsm/fusion": patch
---
Truncate board titles to 140 characters with ellipsis and tooltip
Task card titles in the board view are now truncated to a maximum of 140 characters. When a title exceeds this limit, an ellipsis (…) is appended and the full title is shown in a tooltip on hover. This prevents overly long titles from breaking the board layout while preserving full accessibility.
Add title truncation to TaskCard component to prevent long titles from breaking board layout. Titles longer than 140 characters are now truncated with an ellipsis, and the full title is shown in a tooltip on hover.

View File

@@ -637,8 +637,8 @@ function TaskCardComponent({
<span className="card-error-text">{task.error.length > 60 ? task.error.slice(0, 60) + "…" : task.error}</span>
</div>
)}
<div className="card-title">
{task.title || task.description || task.id}
<div className="card-title" title={task.title || task.description || undefined}>
{truncate(task.title, MAX_TITLE_LENGTH) || truncate(task.description, MAX_TITLE_LENGTH) || task.id}
</div>
{task.steps.length > 0 && (() => {
const completedSteps = task.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
@@ -734,6 +734,12 @@ function TaskCardComponent({
const TOUCH_MOVE_THRESHOLD = 10; // pixels
const TOUCH_TAP_MAX_DURATION = 300; // milliseconds
const MAX_TITLE_LENGTH = 140;
function truncate(s: string | undefined, max: number): string {
if (!s) return "";
return s.length > max ? s.slice(0, max) + "…" : s;
}
export const TaskCard = memo(TaskCardComponent, areTaskCardPropsEqual);
TaskCard.displayName = "TaskCard";

View File

@@ -2513,8 +2513,63 @@ describe("TaskCard title display", () => {
...overrides,
} as Task);
it("displays full description when no title exists (no truncation)", () => {
const longDescription = "A".repeat(100);
it("truncates titles longer than 140 characters with ellipsis", () => {
const longTitle = "A".repeat(150);
const task = makeTask({ title: longTitle });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// The title should be truncated to 140 chars + "…"
const expectedTruncated = "A".repeat(140) + "…";
const cardTitle = screen.getByText(expectedTruncated);
expect(cardTitle).toBeDefined();
expect(cardTitle.textContent?.length).toBe(141); // 140 + ellipsis
});
it("shows full title in tooltip via title attribute", () => {
const longTitle = "A".repeat(150);
const task = makeTask({ title: longTitle });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// The title attribute should contain the full untruncated text
const cardTitle = document.querySelector(".card-title");
expect(cardTitle).toHaveAttribute("title", longTitle);
});
it("does not truncate titles exactly 140 characters", () => {
const exactTitle = "B".repeat(140);
const task = makeTask({ title: exactTitle });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// Exactly 140 characters should NOT be truncated (no ellipsis)
const cardTitle = screen.getByText(exactTitle);
expect(cardTitle).toBeDefined();
expect(cardTitle.textContent).toBe(exactTitle);
expect(cardTitle.textContent?.length).toBe(140);
});
it("truncates description fallback when no title present and description exceeds 140 chars", () => {
const longDescription = "C".repeat(200);
const task = makeTask({ title: undefined, description: longDescription });
render(
@@ -2525,10 +2580,46 @@ describe("TaskCard title display", () => {
/>
);
// The full 100-character description should be visible
const cardTitle = screen.getByText(longDescription);
// The description should be truncated to 140 chars + "…"
const expectedTruncated = "C".repeat(140) + "…";
const cardTitle = screen.getByText(expectedTruncated);
expect(cardTitle).toBeDefined();
expect(cardTitle.textContent).toBe(longDescription);
expect(cardTitle.textContent?.length).toBe(141);
});
it("shows full description in tooltip when used as fallback", () => {
const longDescription = "D".repeat(200);
const task = makeTask({ title: undefined, description: longDescription });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// The title attribute should contain the full untruncated description
const cardTitle = document.querySelector(".card-title");
expect(cardTitle).toHaveAttribute("title", longDescription);
});
it("does not truncate short titles under 140 characters", () => {
const shortTitle = "A".repeat(100);
const task = makeTask({ title: shortTitle });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// Short titles should display unchanged
const cardTitle = screen.getByText(shortTitle);
expect(cardTitle).toBeDefined();
expect(cardTitle.textContent).toBe(shortTitle);
expect(cardTitle.textContent?.length).toBe(100);
});