feat(FN-2046): add accessible delete button to chat session items
- Add an inline delete button to each chat session item with an aria-label and Trash icon - Prevent session selection when delete is clicked by stopping event propagation and opening the confirmation dialog - Style the delete control to appear on hover/focus and remain visible on mobile for touch accessibility - Add ChatView tests covering render behavior, confirmation flow, selection guard, delete action, and CSS visibility rules
This commit is contained in:
@@ -869,6 +869,17 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
}}
|
||||
data-testid={`chat-session-${session.id}`}
|
||||
>
|
||||
<button
|
||||
className="chat-session-delete-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setConfirmDelete(session.id);
|
||||
}}
|
||||
data-testid="chat-session-delete-btn"
|
||||
aria-label="Delete conversation"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
<div className="chat-session-title">{session.title || "Untitled"}</div>
|
||||
<div className="chat-session-preview">
|
||||
{session.lastMessagePreview || "No messages"}
|
||||
|
||||
@@ -1169,6 +1169,113 @@ describe("formatModelTag helper function", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Chat Session Delete Button", () => {
|
||||
it("renders delete button on each session item", () => {
|
||||
setupMockChat({
|
||||
sessions: [
|
||||
{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat 1", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
{ id: "session-002", agentId: "agent-002", status: "active", title: "Test Chat 2", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
filteredSessions: [
|
||||
{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat 1", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
{ id: "session-002", agentId: "agent-002", status: "active", title: "Test Chat 2", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const deleteButtons = screen.getAllByTestId("chat-session-delete-btn");
|
||||
expect(deleteButtons.length).toBe(2);
|
||||
});
|
||||
|
||||
it("clicking delete button shows confirmation dialog", async () => {
|
||||
setupMockChat({
|
||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const deleteButton = screen.getByTestId("chat-session-delete-btn");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
// Dialog should be open
|
||||
const dialog = document.querySelector(".chat-new-dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(within(dialog!).getByText("Delete Conversation?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking delete button does not select the session", async () => {
|
||||
const selectSession = vi.fn();
|
||||
setupMockChat({
|
||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
selectSession,
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const deleteButton = screen.getByTestId("chat-session-delete-btn");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
expect(selectSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("confirming delete calls deleteSession", async () => {
|
||||
const deleteSession = vi.fn();
|
||||
setupMockChat({
|
||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
deleteSession,
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const deleteButton = screen.getByTestId("chat-session-delete-btn");
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
// Click confirm in dialog
|
||||
const dialog = document.querySelector(".chat-new-dialog");
|
||||
await userEvent.click(within(dialog!).getByText("Delete"));
|
||||
|
||||
expect(deleteSession).toHaveBeenCalledWith("session-001");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Chat Session Delete Button CSS", () => {
|
||||
const css = fs.readFileSync(stylesPath, "utf-8");
|
||||
|
||||
it(".chat-session-delete-btn exists with opacity: 0", () => {
|
||||
const match = css.match(/\.chat-session-delete-btn\s*\{([^}]*)\}/);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match![1]).toContain("opacity: 0");
|
||||
});
|
||||
|
||||
it(".chat-session-item:hover .chat-session-delete-btn has opacity: 1", () => {
|
||||
const match = css.match(/\.chat-session-item:hover\s*\.chat-session-delete-btn\s*\{([^}]*)\}/);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match![1]).toContain("opacity: 1");
|
||||
});
|
||||
|
||||
it("mobile override makes delete button always visible", () => {
|
||||
// Find all mobile media query blocks and check if any has chat-session-delete-btn with opacity: 1
|
||||
const mobileRegex = /@media\s*\(max-width:\s*768px\)\s*\{([\s\S]*?)\n\}/g;
|
||||
let match;
|
||||
let foundMobileDeleteBtn = false;
|
||||
while ((match = mobileRegex.exec(css)) !== null) {
|
||||
const mediaContent = match[1];
|
||||
if (mediaContent.includes(".chat-session-delete-btn")) {
|
||||
const deleteBtnMatch = mediaContent.match(/\.chat-session-delete-btn\s*\{([^}]*)\}/);
|
||||
if (deleteBtnMatch && deleteBtnMatch[1].includes("opacity: 1")) {
|
||||
foundMobileDeleteBtn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(foundMobileDeleteBtn).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatView CSS — nested flexbox scrolling fix", () => {
|
||||
const css = fs.readFileSync(stylesPath, "utf-8");
|
||||
|
||||
|
||||
@@ -31104,6 +31104,41 @@ html .column.drag-over * {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* === Chat Session Delete Button === */
|
||||
.chat-session-delete-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.chat-session-item:hover .chat-session-delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-session-delete-btn:hover {
|
||||
color: var(--color-error);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-session-delete-btn:focus {
|
||||
opacity: 1;
|
||||
outline: 2px solid var(--focus-ring-strong);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Context menu for session items */
|
||||
.chat-session-context-menu {
|
||||
position: fixed;
|
||||
@@ -35865,4 +35900,9 @@ html .column.drag-over * {
|
||||
.insights-view-count {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Chat session delete button - always visible on mobile */
|
||||
.chat-session-delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user