From 0a16bc40aeb97e0c07c4fe9a18110b7e2bb11b41 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 10 Oct 2026 13:38:49 +0200 Subject: [PATCH] fix(ui): a crop frame grabbed with the pointer takes the focus, so the arrows go on from there The press is held back from the page (no text selection, no play / pause underneath), which also kept it from focusing: after a drag the arrows still went to whatever had the focus before, and Escape did not reach the frame. The press now focuses the frame or the corner it grabbed (an edge focuses its frame). --- .../src/components/ui/CropFrame.dom.test.tsx | 18 ++++++++++++++++++ frontend/src/components/ui/CropFrame.tsx | 3 +++ 2 files changed, 21 insertions(+) diff --git a/frontend/src/components/ui/CropFrame.dom.test.tsx b/frontend/src/components/ui/CropFrame.dom.test.tsx index e344bc80..a6b6d33c 100644 --- a/frontend/src/components/ui/CropFrame.dom.test.tsx +++ b/frontend/src/components/ui/CropFrame.dom.test.tsx @@ -99,6 +99,24 @@ describe("CropFrame", () => { expect(rect()).toBe("0.300 0.300 0.400 0.400"); }); + it("takes the focus on a press, so the arrows go on from where the pointer left it", () => { + render(); + const frame = screen.getByRole("group", { name: "common.cropFrame.frame" }); + fireEvent.pointerDown(frame, { button: 0, clientX: 500, clientY: 300 }); + fireEvent.pointerUp(frame, { clientX: 500, clientY: 300 }); + expect(document.activeElement).toBe(frame); + const se = screen.getByRole("group", { name: "common.cropFrame.se" }); + fireEvent.pointerDown(se, { button: 0, clientX: 960, clientY: 540 }); + expect(document.activeElement).toBe(se); + // An edge is the pointer's only: its press puts the focus on the frame it belongs to. + fireEvent.pointerDown(document.querySelector(".cursor-ew-resize")!, { + button: 0, + clientX: 960, + clientY: 400, + }); + expect(document.activeElement).toBe(frame); + }); + it("lets a press outside the frame through to the player, and keeps one on the frame", () => { const click = vi.fn(); render( diff --git a/frontend/src/components/ui/CropFrame.tsx b/frontend/src/components/ui/CropFrame.tsx index 460f7c04..c377ccdc 100644 --- a/frontend/src/components/ui/CropFrame.tsx +++ b/frontend/src/components/ui/CropFrame.tsx @@ -90,6 +90,9 @@ export function CropFrame({ aspect, rect, ratio, onChange, className }: Props) { // The press is the frame's: not the video's play / pause, not a scrub of anything under it. e.stopPropagation(); e.preventDefault(); + // preventDefault also stops the press from focusing: focused by hand, so the arrows go on from + // here (an edge is the pointer's only, so its frame takes the focus). + e.currentTarget.closest("[tabindex]")?.focus({ preventScroll: true }); try { e.currentTarget.setPointerCapture(e.pointerId); } catch {