fix(ui): a small crop frame takes smaller handles, so it can still be grabbed to move
The handles reach 10 px into the frame from every side; a frame at its minimum on a small picture (the Downloads editor at 800 px: 41 x 23) was then nearly all handle - 74 of its 920 px still moved it. Below 64 px on its shorter side the frame takes 12 px corners and 8 px edges: 262 px move it.
This commit is contained in:
@@ -33,8 +33,16 @@ beforeAll(() => {
|
||||
|
||||
const WIDE = 16 / 9;
|
||||
|
||||
function Harness({ aspect = WIDE, ratio = null }: { aspect?: number; ratio?: number | null }) {
|
||||
const [rect, setRect] = useState<Frac>({ x: 0.2, y: 0.2, w: 0.4, h: 0.4 });
|
||||
function Harness({
|
||||
aspect = WIDE,
|
||||
ratio = null,
|
||||
start = { x: 0.2, y: 0.2, w: 0.4, h: 0.4 },
|
||||
}: {
|
||||
aspect?: number;
|
||||
ratio?: number | null;
|
||||
start?: Frac;
|
||||
}) {
|
||||
const [rect, setRect] = useState<Frac>(start);
|
||||
return (
|
||||
<div>
|
||||
<output data-testid="rect">
|
||||
@@ -117,6 +125,20 @@ describe("CropFrame", () => {
|
||||
expect(document.activeElement).toBe(frame);
|
||||
});
|
||||
|
||||
it("shrinks its handles on a small frame, so the frame itself stays there to grab", () => {
|
||||
const corner = () => screen.getByRole("group", { name: "common.cropFrame.se" });
|
||||
const edge = () => document.querySelector<HTMLElement>(".cursor-ew-resize")!;
|
||||
// 640 × 360 px of a 1600 × 900 picture: the full handles.
|
||||
render(<Harness />);
|
||||
expect(corner().className).toContain("size-5");
|
||||
expect(edge().className).toContain("w-4");
|
||||
cleanup();
|
||||
// A 1:4 picture in that box is 225 px wide: a tenth of it is a 22 px frame.
|
||||
render(<Harness aspect={0.25} start={{ x: 0.2, y: 0.2, w: 0.1, h: 0.5 }} />);
|
||||
expect(corner().className).toContain("size-3");
|
||||
expect(edge().className).toContain("w-2");
|
||||
});
|
||||
|
||||
it("lets a press outside the frame through to the player, and keeps one on the frame", () => {
|
||||
const click = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -60,6 +60,20 @@ const HANDLE_BOX: Record<CropHandle, string> = {
|
||||
e: "inset-y-3 -right-2 w-4 cursor-ew-resize",
|
||||
w: "inset-y-3 -left-2 w-4 cursor-ew-resize",
|
||||
};
|
||||
// The same on a small frame: the full handles reach 10 px into it from every side, and a frame at its
|
||||
// minimum on a small picture (40 × 23 px) was then nearly all handle, with no room left to grab it.
|
||||
const SMALL_HANDLE_BOX: Record<CropHandle, string> = {
|
||||
nw: "-left-1.5 -top-1.5 size-3 cursor-nwse-resize",
|
||||
ne: "-right-1.5 -top-1.5 size-3 cursor-nesw-resize",
|
||||
sw: "-left-1.5 -bottom-1.5 size-3 cursor-nesw-resize",
|
||||
se: "-right-1.5 -bottom-1.5 size-3 cursor-nwse-resize",
|
||||
n: "inset-x-1.5 -top-1 h-2 cursor-ns-resize",
|
||||
s: "inset-x-1.5 -bottom-1 h-2 cursor-ns-resize",
|
||||
e: "inset-y-1.5 -right-1 w-2 cursor-ew-resize",
|
||||
w: "inset-y-1.5 -left-1 w-2 cursor-ew-resize",
|
||||
};
|
||||
// Below this many px on its shorter side a frame takes the small handles.
|
||||
const SMALL_FRAME_PX = 64;
|
||||
|
||||
type Drag = { handle: CropHandle | "move"; x: number; y: number; from: Frac };
|
||||
// Not the app's accent ring: over a video it can be any colour, and a light theme's dark-blue accent
|
||||
@@ -84,6 +98,8 @@ export function CropFrame({ aspect, rect, ratio, onChange, className }: Props) {
|
||||
}, []);
|
||||
|
||||
const pic = contentRect(box.w, box.h, aspect);
|
||||
const small = !!pic && Math.min(rect.w * pic.width, rect.h * pic.height) < SMALL_FRAME_PX;
|
||||
const handleBox = small ? SMALL_HANDLE_BOX : HANDLE_BOX;
|
||||
|
||||
const begin = (handle: Drag["handle"]) => (e: PointerEvent<HTMLElement>) => {
|
||||
if (e.button !== 0) return;
|
||||
@@ -180,7 +196,7 @@ export function CropFrame({ aspect, rect, ratio, onChange, className }: Props) {
|
||||
<div
|
||||
key={h}
|
||||
aria-hidden="true"
|
||||
className={clsx("absolute touch-none", HANDLE_BOX[h])}
|
||||
className={clsx("absolute touch-none", handleBox[h])}
|
||||
{...pointer(h)}
|
||||
/>
|
||||
))}
|
||||
@@ -193,13 +209,15 @@ export function CropFrame({ aspect, rect, ratio, onChange, className }: Props) {
|
||||
data-crop={h}
|
||||
className={clsx(
|
||||
"absolute flex touch-none items-center justify-center rounded-xs",
|
||||
HANDLE_BOX[h],
|
||||
handleBox[h],
|
||||
FOCUS,
|
||||
)}
|
||||
onKeyDown={keys(h)}
|
||||
{...pointer(h)}
|
||||
>
|
||||
<span className="size-2.5 rounded-xs bg-white shadow" />
|
||||
<span
|
||||
className={clsx("rounded-xs bg-white shadow", small ? "size-1.5" : "size-2.5")}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user