Skip to content

Commit 2e2e812

Browse files
committed
fix(canvas): hide rotate button during drag/transform, clean up cursor on unmount
Addresses gemini review on PR #53: - Hide the quick-rotate button while a drag or transform is in progress. The button's position is React-state-driven and would otherwise lag behind the live Konva node until the interaction ends. - Reset the stage cursor when the button unmounts. Without this, deleting or deselecting the object while the cursor is over the button would leave the pointer stuck in 'pointer' style because onMouseLeave never fires.
1 parent 5636ef6 commit 2e2e812

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

‎src/components/Canvas/LabelCanvas.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,21 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
394394
: null;
395395
const stepRotation = singleSelected ? getStepRotation(singleSelected) : null;
396396
const [rotationBtnPos, setRotationBtnPos] = useState<{ x: number; y: number } | null>(null);
397+
// Hide the rotate affordance during an active drag / transform — the
398+
// button's position is React-state-driven and would otherwise lag behind
399+
// the live Konva node until the interaction ends.
400+
const [isInteracting, setIsInteracting] = useState(false);
401+
useEffect(() => {
402+
const stage = stageRef.current;
403+
if (!stage) return;
404+
const start = () => setIsInteracting(true);
405+
const end = () => setIsInteracting(false);
406+
stage.on("dragstart.rotbtn transformstart.rotbtn", start);
407+
stage.on("dragend.rotbtn transformend.rotbtn", end);
408+
return () => {
409+
stage.off("dragstart.rotbtn transformstart.rotbtn dragend.rotbtn transformend.rotbtn");
410+
};
411+
}, []);
397412
useLayoutEffect(() => {
398413
if (!singleSelected || !stepRotation) {
399414
setRotationBtnPos(null);
@@ -804,7 +819,7 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
804819
ignoreStroke
805820
/>
806821

807-
{rotationBtnPos && (
822+
{rotationBtnPos && !isInteracting && (
808823
<RotationButton
809824
x={rotationBtnPos.x}
810825
y={rotationBtnPos.y}

‎src/components/Canvas/RotationButton.tsx‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import { Group, Circle, Path } from "react-konva";
33
import type Konva from "konva";
44

@@ -32,18 +32,33 @@ const ICON_OFFSET = 12; // 24x24 viewBox → centre at (12, 12)
3232

3333
export function RotationButton({ x, y, color, onClick }: Props) {
3434
const [hover, setHover] = useState(false);
35+
// Track the stage we set a cursor on so unmount-while-hovering can still
36+
// clean up (onMouseLeave never fires in that case — e.g. user hits Delete
37+
// or Esc while the cursor is over the button).
38+
const cursorStageRef = useRef<Konva.Stage | null>(null);
3539

3640
const cursorIn = (e: Konva.KonvaEventObject<MouseEvent>) => {
3741
const stage = e.target.getStage();
38-
if (stage) stage.container().style.cursor = "pointer";
42+
if (stage) {
43+
stage.container().style.cursor = "pointer";
44+
cursorStageRef.current = stage;
45+
}
3946
setHover(true);
4047
};
4148
const cursorOut = (e: Konva.KonvaEventObject<MouseEvent>) => {
4249
const stage = e.target.getStage();
4350
if (stage) stage.container().style.cursor = "";
51+
cursorStageRef.current = null;
4452
setHover(false);
4553
};
4654

55+
useEffect(() => {
56+
return () => {
57+
const stage = cursorStageRef.current;
58+
if (stage) stage.container().style.cursor = "";
59+
};
60+
}, []);
61+
4762
return (
4863
<Group
4964
x={x}

0 commit comments

Comments
 (0)