Learnify - Hand Mode Hover Functionality Issue Analysis and Solution

Problem Description

When users switch from the default Select tool to Hand Mode (pan tool), the following issues occur:

  1. Complete hover functionality failure: No visual feedback when hovering over mind map nodes
  2. Collapse buttons invisible: Expand/collapse buttons for mind map nodes cannot be displayed
  3. Cursor doesn't change: Cursor doesn't change to pointer when hovering over interactive elements
  4. Click operations fail: Unable to expand/collapse mind map nodes through clicking

Root Cause Analysis

1. Tool switching causes event handling transfer

When switching to Hand Mode, all mouse events are routed to PanTool for handling:

1
2
3
// In tool-controller.ts's invokeToolHandler
tool = tool ?? this.currentTool$.peek(); // Now it's PanTool
tool?.[evtName](evt); // All events are handled by PanTool

2. Incomplete original PanTool implementation

The original PanTool only implements core methods related to panning:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Original PanTool only has these methods
export class PanTool extends BaseTool<PanToolOption> {
override dragStart(e: PointerEventState): void {
/* Panning logic */
}
override dragMove(e: PointerEventState): void {
/* Panning logic */
}
override dragEnd(e: PointerEventState): void {
/* Panning logic */
}

// Missing the following key methods:
// - pointerMove (hover functionality)
// - click (click interaction)
// - pointerDown/pointerUp (press/release)
// - doubleClick (double click)
}

3. Missing Interactivity system

DefaultTool handles complex interaction logic through the interactivity system:

1
2
3
4
5
6
7
8
9
10
11
12
// Key implementation in DefaultTool
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}

override pointerMove(e: PointerEventState) {
this.interactivity?.dispatchEvent('pointermove', e); // Handle hover
}

override click(e: PointerEventState) {
this.interactivity?.dispatchEvent('click', e); // Handle click
}

The original PanTool doesn't integrate with this system, causing all interactivity-related functionality to fail.

Solution Design

Design Principles

  1. Functional completeness: Hand Mode should support panning + complete mind map interaction
  2. Backward compatibility: Don't break existing panning functionality
  3. Code reuse: Utilize existing interactivity system instead of reimplementing
  4. Performance optimization: Avoid duplicate event processing

Technical Approach

Approach 1: Event re-routing (Attempted but problematic)

Re-route specific events from PanTool to DefaultTool in invokeToolHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// In tool-controller.ts
const invokeToolHandler = (evtName: SupportedEvents, evt: PointerEventState, tool?: BaseTool) => {
tool = tool ?? this.currentTool$.peek();

// Re-route non-drag events of PanTool to DefaultTool
if (tool?.toolName === 'pan' && (evtName === 'click' || evtName === 'pointerMove')) {
const defaultTool = this._tools.get('default');
if (defaultTool) {
tool = defaultTool;
}
}

// ... Normal flow
};

Problem: This approach causes state inconsistency as two tools may have different internal states.

Approach 2: PanTool Enhancement (Final adopted approach)

Directly add complete interaction support to PanTool:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export class PanTool extends BaseTool<PanToolOption> {
// 1. Add interactivity system support
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}

// 2. Maintain original panning functionality
override dragStart(e: PointerEventState): void {
/* Panning logic */
}
override dragMove(e: PointerEventState): void {
/* Panning logic */
}
override dragEnd(e: PointerEventState): void {
// First dispatch event to interactivity system
this.interactivity?.dispatchEvent('dragend', e);
// Then execute panning logic
/* Panning logic */
}

// 3. Add complete interaction methods
override click(e: PointerEventState): void {
this.interactivity?.dispatchEvent('click', e);
}

override pointerMove(e: PointerEventState): void {
this.interactivity?.dispatchEvent('pointermove', e);
}

override pointerDown(e: PointerEventState): void {
this.interactivity?.dispatchEvent('pointerdown', e);
}

override pointerUp(e: PointerEventState): void {
this.interactivity?.dispatchEvent('pointerup', e);
}

override doubleClick(e: PointerEventState): void {
this.interactivity?.dispatchEvent('dblclick', e);
}
}

Implementation Details

1. Import necessary modules

1
2
3
4
5
6
import {
BaseTool,
InteractivityIdentifier, // New addition
MouseButton,
type ToolOptions,
} from '@blocksuite/std/gfx';

2. Add interactivity getter

1
2
3
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}

3. Enhance event handling methods

Add interactivity dispatch for each interaction event:

1
2
3
4
5
6
7
8
9
override click(e: PointerEventState): void {
// Dispatch click event to interactivity system, making mind map expand/collapse functionality work properly
this.interactivity?.dispatchEvent('click', e);
}

override pointerMove(e: PointerEventState): void {
// Dispatch pointer move event to interactivity system, making hover functionality work properly
this.interactivity?.dispatchEvent('pointermove', e);
}

4. Modify dragEnd method

Ensure proper interactivity handling when drag ends:

1
2
3
4
5
6
7
8
override dragEnd(e: PointerEventState): void {
// Dispatch dragend event to interactivity system
this.interactivity?.dispatchEvent('dragend', e);

// Panning-related state cleanup
this._lastPoint = null;
this.panning$.value = false;
}

Testing and Verification

Functional Testing Checklist

  • View panning: Dragging blank areas can pan the view normally
  • Mind map hover: Hovering over nodes shows highlight effects
  • Collapse button display: Correctly shows expand/collapse buttons when hovering
  • Cursor changes: Cursor changes to pointer when hovering over interactive elements
  • Click expand/collapse: Clicking buttons can normally expand/collapse nodes
  • Double-click functionality: Double-click operations work normally
  • Middle-click panning: Middle-click temporary panning functionality remains normal

Debugging Process

1. Confirm event dispatch

Add debug logs to confirm events are properly dispatched:

1
2
3
4
override pointerMove(e: PointerEventState): void {
console.log('PanTool.pointerMove - dispatching to interactivity');
this.interactivity?.dispatchEvent('pointermove', e);
}

2. Verify interactivity system working

Check if the interactivity system correctly receives and processes events:

1
2
3
4
5
get interactivity() {
const result = this.std.getOptional(InteractivityIdentifier);
console.log('PanTool.interactivity getter:', result ? 'found' : 'not found');
return result;
}

Performance Considerations

1. Event dispatch overhead

Each event goes through the interactivity system first, then executes tool-specific logic. This adds some overhead, but:

  • Minimal overhead: Just method calls, no complex computations
  • Necessary: This is the only way to get complete interaction functionality
  • Consistency: Maintains the same processing approach as DefaultTool

2. Memory usage

No additional memory overhead, just reusing the existing interactivity system.

Alternative Approach Comparison

Approach Pros Cons Complexity
Event re-routing Don't modify PanTool State inconsistency, hard to maintain Medium
PanTool enhancement Complete functionality, consistent state Need to modify PanTool Low
Dual tool coexistence Maintain original logic Complex state synchronization High
Block tool switching Simple and direct Poor user experience Low

Best Practices Summary

  1. Tools should be functionally complete: Each tool should provide a complete user interaction experience
  2. Reuse interactivity system: Utilize existing interaction framework instead of reimplementing
  3. Maintain event handling consistency: All tools should handle the same types of events in the same way
  4. Prioritize user experience: Between technical implementation and user experience, prioritize user experience
  • blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts - Modified PanTool
  • blocksuite/affine/blocks/surface/src/tool/default-tool.ts - Reference DefaultTool implementation
  • blocksuite/framework/std/src/gfx/tool/tool-controller.ts - Event routing logic

Reference Documentation