This is an automated email from the ASF dual-hosted git repository.

thiagoelg pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new 3d1b6f0d5ea NO-ISSUE: Fix Sub-Process handles on hover in the BPMN 
Editor (#3608)
3d1b6f0d5ea is described below

commit 3d1b6f0d5eac89ce087cef262a8d7b236ebcb7f2
Author: Thiago Lugli <[email protected]>
AuthorDate: Fri May 29 23:33:20 2026 -0300

    NO-ISSUE: Fix Sub-Process handles on hover in the BPMN Editor (#3608)
---
 packages/bpmn-editor/src/diagram/nodes/Nodes.tsx   | 27 +++++++++-
 packages/bpmn-editor/src/hover/useHoverPosition.ts | 59 ++++++++++++++++++++++
 2 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/packages/bpmn-editor/src/diagram/nodes/Nodes.tsx 
b/packages/bpmn-editor/src/diagram/nodes/Nodes.tsx
index b3a06f12cce..8d115202457 100644
--- a/packages/bpmn-editor/src/diagram/nodes/Nodes.tsx
+++ b/packages/bpmn-editor/src/diagram/nodes/Nodes.tsx
@@ -56,6 +56,7 @@ import { OutgoingStuffNodePanel } from 
"@kie-tools/xyflow-react-kie-diagram/dist
 import { PositionalNodeHandles } from 
"@kie-tools/xyflow-react-kie-diagram/dist/nodes/PositionalNodeHandles";
 import { useIsHovered } from 
"@kie-tools/xyflow-react-kie-diagram/dist/reactExt/useIsHovered";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useHoverPosition } from "../../hover/useHoverPosition";
 import { updateFlowElement, updateLane, updateTextAnnotation } from 
"../../mutations/renameNode";
 import { Normalized } from "../../normalization/normalize";
 import { useBpmnEditorStore, useBpmnEditorStoreApi } from 
"../../store/StoreContext";
@@ -929,12 +930,36 @@ export const SubProcessNode = React.memo(
       (s) => (isHovered || isResizing) && 
s.xyFlowReactKieDiagram.draggingNodes.length === 0
     );
 
+    // useIsHovered(ref) doesn't work here for two reasons:
+    // 1. Child nodes (e.g. Tasks inside this Sub-Process) are separate DOM 
elements rendered at the
+    //    same level as this node but with a higher z-index, so they sit on 
top and block mouseenter.
+    // 2. InfoNodePanel and OutgoingStuffNodePanel float outside this node's 
bounding box but are
+    //    still DOM descendants — when they unmount because isTargeted 
changed, the browser fires a
+    //    fake mouseleave with the cursor at a position outside 
getBoundingClientRect(), which
+    //    would incorrectly clear the hover state and cause a toggle loop.
+    // Tracking the cursor position on every mousemove is immune to both 
problems.
+    const isConnectionBeingMade = RF.useStore((s) => !!s.connectionNodeId && 
s.connectionNodeId !== id);
+    const [isHoveredByBounds, setIsHoveredByBounds] = useState(false);
+    const checkBounds = useCallback((x: number, y: number) => {
+      const r = ref.current?.parentElement?.getBoundingClientRect();
+      setIsHoveredByBounds(!!r && x >= r.left && x <= r.right && y >= r.top && 
y <= r.bottom);
+    }, []);
+    useHoverPosition(isConnectionBeingMade, checkBounds);
+    useEffect(() => {
+      if (!isConnectionBeingMade) {
+        setIsHoveredByBounds(false);
+      }
+    }, [isConnectionBeingMade]);
+
     const { isEditingLabel, setEditingLabel, triggerEditing, 
triggerEditingIfEnter } = useEditableNodeLabel(id);
     useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, 
selected, isEditingLabel);
 
     const bpmnEditorStoreApi = useBpmnEditorStoreApi();
 
-    const { isTargeted, isValidConnectionTarget } = 
useConnectionTargetStatus(id, shouldActLikeHovered);
+    const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(
+      id,
+      shouldActLikeHovered || isHoveredByBounds
+    );
     const className = useNodeClassName(isValidConnectionTarget, id, 
NODE_TYPES, EDGE_TYPES);
     const nodeDimensions = useNodeDimensions({ shape, nodeType: type as 
BpmnNodeType, MIN_NODE_SIZES });
 
diff --git a/packages/bpmn-editor/src/hover/useHoverPosition.ts 
b/packages/bpmn-editor/src/hover/useHoverPosition.ts
new file mode 100644
index 00000000000..e17e28d0058
--- /dev/null
+++ b/packages/bpmn-editor/src/hover/useHoverPosition.ts
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect } from "react";
+
+type Callback = (x: number, y: number) => void;
+
+// Module-level singleton: one DOM listener shared across all consumers.
+const subscribers = new Set<Callback>();
+let rafId: ReturnType<typeof requestAnimationFrame> | undefined;
+const latestPos = { x: 0, y: 0 };
+
+function onMouseMove(e: MouseEvent) {
+  latestPos.x = e.clientX;
+  latestPos.y = e.clientY;
+  if (rafId !== undefined) return;
+  rafId = requestAnimationFrame(() => {
+    rafId = undefined;
+    for (const fn of subscribers) {
+      fn(latestPos.x, latestPos.y);
+    }
+  });
+}
+
+export function useHoverPosition(active: boolean, callback: Callback) {
+  useEffect(() => {
+    if (!active) return;
+    if (subscribers.size === 0) {
+      document.addEventListener("mousemove", onMouseMove);
+    }
+    subscribers.add(callback);
+    return () => {
+      subscribers.delete(callback);
+      if (subscribers.size === 0) {
+        document.removeEventListener("mousemove", onMouseMove);
+        if (rafId !== undefined) {
+          cancelAnimationFrame(rafId);
+          rafId = undefined;
+        }
+      }
+    };
+  }, [active, callback]);
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to