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:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user