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:
@@ -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.
|
||||
@@ -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