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:
@@ -206,7 +206,7 @@ function TaskCardComponent({
|
|||||||
const hasTouchMovedRef = useRef(false);
|
const hasTouchMovedRef = useRef(false);
|
||||||
|
|
||||||
const isInteractiveTarget = useCallback((target: EventTarget | null): boolean => {
|
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']");
|
return !!target.closest("button, a, input, textarea, select, label, [role='button']");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,85 @@ describe("TaskCard mobile", () => {
|
|||||||
expect(fetchTaskDetail).not.toHaveBeenCalled();
|
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", () => {
|
it("renders edit button with aria-label in editable columns", () => {
|
||||||
const task = createTask({ id: "FN-202", column: "todo" });
|
const task = createTask({ id: "FN-202", column: "todo" });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user