fix(KB-183): prevent dep-dropdown clicks from dismissing InlineCreateCard

- Add onMouseDown preventDefault on dep-dropdown and items to retain focus
- Extend focusout guard to check dependencies and showDeps state before cancelling
- Add useEffect dependency array entries for dependencies and showDeps
- Add tests for mouseDown preventDefault and blur-with-deps retention
This commit is contained in:
Dustin Byrne
2026-03-28 18:15:48 -04:00
parent 494de14820
commit 3e7966042c
2 changed files with 42 additions and 4 deletions

View File

@@ -43,14 +43,14 @@ export function InlineCreateCard({ tasks, onSubmit, onCancel, addToast }: Inline
const handleFocusOut = (e: FocusEvent) => {
// relatedTarget is the element receiving focus — if it's inside the card, ignore
if (e.relatedTarget instanceof Node && card.contains(e.relatedTarget)) return;
// Only cancel if empty
if (description.trim() === "" && pendingImages.length === 0) {
// Only cancel if empty and dropdown is not open
if (description.trim() === "" && pendingImages.length === 0 && dependencies.length === 0 && !showDeps) {
onCancel();
}
};
card.addEventListener("focusout", handleFocusOut);
return () => card.removeEventListener("focusout", handleFocusOut);
}, [description, pendingImages, onCancel]);
}, [description, pendingImages, dependencies, showDeps, onCancel]);
// Clean up object URLs on unmount to prevent memory leaks
useEffect(() => {
@@ -205,7 +205,7 @@ export function InlineCreateCard({ tasks, onSubmit, onCancel, addToast }: Inline
)
: tasks;
return (
<div className="dep-dropdown">
<div className="dep-dropdown" onMouseDown={(e) => e.preventDefault()}>
<input
className="dep-dropdown-search"
placeholder="Search tasks…"
@@ -221,6 +221,7 @@ export function InlineCreateCard({ tasks, onSubmit, onCancel, addToast }: Inline
<div
key={t.id}
className={`dep-dropdown-item${dependencies.includes(t.id) ? " selected" : ""}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => toggleDep(t.id)}
>
<span className="dep-dropdown-id">{t.id}</span>

View File

@@ -67,6 +67,43 @@ describe("InlineCreateCard blur-to-cancel", () => {
});
});
describe("InlineCreateCard dep-dropdown focus retention", () => {
const testTasks: Task[] = [
{ id: "KB-010", title: "Task A", description: "First task", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },
];
it("dep-dropdown-item mouseDown calls preventDefault to retain focus", () => {
renderCard(testTasks);
// Open the dropdown
fireEvent.click(screen.getByText(/Deps/));
const item = document.querySelector(".dep-dropdown-item") as HTMLElement;
expect(item).toBeTruthy();
// Fire mouseDown and verify preventDefault was called —
// this is the mechanism that keeps focus on the search input in
// real browsers and prevents a focusout with relatedTarget: null
const prevented = !fireEvent.mouseDown(item);
expect(prevented).toBe(true);
});
it("does NOT call onCancel when focus leaves card with selected dependencies but empty description", () => {
const { props } = renderCard(testTasks);
const textarea = screen.getByPlaceholderText("What needs to be done?");
// Open dropdown and select a dependency
fireEvent.click(screen.getByText(/Deps/));
const item = document.querySelector(".dep-dropdown-item") as HTMLElement;
expect(item).toBeTruthy();
fireEvent.click(item);
// Focus the textarea then blur out of the card entirely
textarea.focus();
fireEvent.focusOut(textarea, { relatedTarget: null });
expect(props.onCancel).not.toHaveBeenCalled();
});
});
describe("InlineCreateCard dependency dropdown search", () => {
const testTasks: Task[] = [
{ id: "KB-001", title: "Fix login", description: "Login page broken", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },