feat(FN-1297): prevent mobile detail open from control taps

- Broaden TaskCard touch target detection from HTMLElement to Element so nested SVG targets are treated as interactive
- Keep mobile tap handling from opening task details when pressing card controls
- Add board-mobile regressions for edit button taps, steps toggle taps, and SVG-inside-button touch events
This commit is contained in:
gsxdsm
2026-04-08 12:10:16 -07:00
parent a997436b7c
commit 786c9309e1
2 changed files with 80 additions and 1 deletions

View File

@@ -206,7 +206,7 @@ function TaskCardComponent({
const hasTouchMovedRef = useRef(false);
const isInteractiveTarget = useCallback((target: EventTarget | null): boolean => {
if (!(target instanceof HTMLElement)) return false;
if (!(target instanceof Element)) return false;
return !!target.closest("button, a, input, textarea, select, label, [role='button']");
}, []);

View File

@@ -252,6 +252,85 @@ describe("TaskCard mobile", () => {
expect(fetchTaskDetail).not.toHaveBeenCalled();
});
it("does not open task detail when tapping the edit button", async () => {
const task = createTask({ id: "FN-204", column: "todo" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={vi.fn()}
onUpdateTask={vi.fn().mockResolvedValue(task)}
/>,
);
const editButton = screen.getByRole("button", { name: "Edit task" });
fireEvent.touchStart(editButton, {
touches: [{ clientX: 100, clientY: 100 }],
});
fireEvent.touchEnd(editButton, {
changedTouches: [{ clientX: 100, clientY: 100 }],
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(fetchTaskDetail).not.toHaveBeenCalled();
});
it("does not open task detail when tapping the steps toggle", async () => {
const task = createTask({
id: "FN-205",
column: "todo",
steps: [
{ name: "Step 1", status: "done" },
{ name: "Step 2", status: "in-progress" },
],
});
render(
<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />,
);
const toggleButton = screen.getByRole("button", { name: "Show steps" });
fireEvent.touchStart(toggleButton, {
touches: [{ clientX: 100, clientY: 100 }],
});
fireEvent.touchEnd(toggleButton, {
changedTouches: [{ clientX: 100, clientY: 100 }],
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(fetchTaskDetail).not.toHaveBeenCalled();
});
it("does not open task detail when touch target is an SVG element inside a button", async () => {
const task = createTask({
id: "FN-206",
column: "todo",
steps: [
{ name: "Step 1", status: "done" },
{ name: "Step 2", status: "in-progress" },
],
});
render(
<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />,
);
const toggleButton = screen.getByRole("button", { name: "Show steps" });
const svgTarget = toggleButton.querySelector("svg");
expect(svgTarget).toBeTruthy();
fireEvent.touchStart(svgTarget as SVGElement, {
touches: [{ clientX: 100, clientY: 100 }],
});
fireEvent.touchEnd(svgTarget as SVGElement, {
changedTouches: [{ clientX: 100, clientY: 100 }],
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(fetchTaskDetail).not.toHaveBeenCalled();
});
it("renders edit button with aria-label in editable columns", () => {
const task = createTask({ id: "FN-202", column: "todo" });