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}`}
|
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-title">{session.title || "Untitled"}</div>
|
||||||
<div className="chat-session-preview">
|
<div className="chat-session-preview">
|
||||||
{session.lastMessagePreview || "No messages"}
|
{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", () => {
|
describe("ChatView CSS — nested flexbox scrolling fix", () => {
|
||||||
const css = fs.readFileSync(stylesPath, "utf-8");
|
const css = fs.readFileSync(stylesPath, "utf-8");
|
||||||
|
|
||||||
|
|||||||
@@ -31104,6 +31104,41 @@ html .column.drag-over * {
|
|||||||
color: var(--text-tertiary);
|
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 */
|
/* Context menu for session items */
|
||||||
.chat-session-context-menu {
|
.chat-session-context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -35865,4 +35900,9 @@ html .column.drag-over * {
|
|||||||
.insights-view-count {
|
.insights-view-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chat session delete button - always visible on mobile */
|
||||||
|
.chat-session-delete-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user