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