Skip to content

Conversation

pavel-klimov
Copy link
Contributor

@pavel-klimov pavel-klimov commented Jul 8, 2025

Switched from MouseEvent to PointerEvent to add basic touch device support (single-finger interaction). Complex gestures (multi-touch) are not supported; behaviour remains the same as with mouse input.

Summary by Sourcery

Switch to a unified PointerEvent-based input model to enable basic single-finger touch interaction across the graph canvas, blocks, connections, and plugins.

New Features:

  • Add basic single-touch support by replacing mouse/touch events with pointer events for canvas interactions and drag gestures.

Enhancements:

  • Unify event listener registrations and custom event dispatches to use PointerEvent throughout graph layers, controllers, and utility functions.
  • Update utility helpers (getXY, getCoord, event key checks) to accept PointerEvent and compute coordinates consistently.
  • Refine camera dragging to use pointer deltas and apply a small movement threshold to prevent unintended pan jitter.
  • Introduce CSS touch-action rules in block, anchor, and layer styles to disable default touch behaviors.

Copy link

sourcery-ai bot commented Jul 8, 2025

Reviewer's Guide

This PR replaces separate mouse and touch handling with unified PointerEvent support across the graph, updating event types, handlers, utilities, drag listener, type definitions, and CSS to enable basic single-touch interactions.

Class diagram for updated event handling types

classDiagram
    class GraphLayer {
        - prevTargetComponent: EventedComponent
        - pointerStartTarget?: EventedComponent
        - pointerStartEvent?: PointerEvent
        - pointerPressed: boolean
        - eventByTargetComponent?: EventedComponent | PointerEvent
        - capturedTargetComponent?: EventedComponent
        + handleEvent(e: Event): void
        + dispatchNativeEvent(type: GraphPointerEventNames, event: PointerEvent | GraphPointerEvent, targetComponent?): void
        + applyEventToTargetComponent(event: PointerEvent | GraphPointerEvent, target?): void
        + updateTargetComponent(event: PointerEvent, force = false): void
        + onRootPointerMove(event: PointerEvent): void
        + handlePointerDownEvent(event: PointerEvent): void
        + onRootPointerStart(event: PointerEvent): void
        + onRootPointerEnd(event: PointerEvent): void
        + tryEmulateClick(event: PointerEvent, target?): void
    }
    class BlockController {
        - block: Block
        + constructor(block: Block)
    }
    class Camera {
        - lastDragEvent?: PointerEvent
        + handlePointerDownEvent(event: PointerEvent): void
        + onDragStart(event: PointerEvent): void
        + onDragUpdate(event: PointerEvent): void
    }
    class Anchor {
        + handleEvent(event: PointerEvent | KeyboardEvent)
    }
    class BaseConnection {
        + handleEvent(event)
    }
    class MiniMapLayer {
        + handlePointerDownEvent(rootEvent: PointerEvent)
        + onCameraDrag(event: PointerEvent)
    }
    class SelectionLayer {
        + handlePointerDown(nativeEvent: GraphPointerEvent)
        + updateSelectionRender(event: PointerEvent)
        + startSelectionRender(event: PointerEvent)
        + endSelectionRender(event: PointerEvent)
    }
    class NewBlockLayer {
        + handlePointerDown(nativeEvent: GraphPointerEvent)
        + onStartNewBlock(event: PointerEvent, block: Block)
        + onMoveNewBlock(event: PointerEvent)
        + onEndNewBlock(event: PointerEvent, point: TPoint)
    }
    class DevToolsLayer {
        - pointerMoveListener
        - pointerEnterListener
        - pointerLeaveListener
        + handlePointerMove(event: PointerEvent)
        + handlePointerEnter()
        + handlePointerLeave()
    }
    class Graph {
        + getPointInCameraSpace(event: PointerEvent)
    }
    class GraphComponent {
        + onDragStart?: (_event: PointerEvent) => void | boolean
        + onDragUpdate?: (diff, _event: PointerEvent) => void
        + onDrop?: (_event: PointerEvent) => void
        + isDraggable?: (event: PointerEvent) => boolean
    }
Loading

Class diagram for updated event utility functions

classDiagram
    class UtilsFunctions {
        + getXY(root: HTMLElement, event: Event | WheelEvent | PointerEvent): [number, number]
        + getCoord(event: PointerEvent, coord: string)
        + getEventDelta(e1, e2)
        + isMetaKeyEvent(event: MouseEvent | PointerEvent | KeyboardEvent): boolean
        + isShiftKeyEvent(event: PointerEvent | KeyboardEvent): boolean
        + isAltKeyEvent(event: PointerEvent | KeyboardEvent): boolean
        + getEventSelectionAction(event: PointerEvent)
        + createCustomDragEvent(eventType: string, e): CustomEvent
        + dispatchEvents(comps, e)
        + addEventListeners(instance: EventTarget, mapEventsToFn?: Record<string, (event: CustomEvent | PointerEvent) => void>): () => void
    }
Loading

Class diagram for updated event type definitions

classDiagram
    class GraphPointerEvent {
        + target?: EventedComponent
        + sourceEvent: PointerEvent
        + pointerPressed?: boolean
    }
    class GraphPointerEventNames {
        <<enumeration>>
        click
        dblclick
        pointerdown
        pointerenter
        pointerleave
    }
    class BaseGraphEventDefinition {
        + click(event: GraphPointerEvent)
        + dblclick(event: GraphPointerEvent)
        + pointerdown(event: GraphPointerEvent)
        + pointerenter(event: GraphPointerEvent)
        + pointerleave(event: GraphPointerEvent)
    }
Loading

File-Level Changes

Change Details Files
Unified mouse/touch handlers to PointerEvent across components and layers
  • Replaced root capturing/bubbling event sets with pointer event types
  • Renamed GraphMouseEvent/GraphMouseEventNames to GraphPointerEvent and updated extraction and isNativeGraphEventName
  • Updated handleEvent branches, dispatch/applyEventToTargetComponent, updateTargetComponent, onRootPointerMove/Start/End to use PointerEvent
  • Changed onGraphEvent/onCanvasEvent/addEventListener invocations in layers/controllers to pointerdown/pointermove/pointerenter/pointerleave
src/components/canvas/layers/graphLayer/GraphLayer.ts
src/graphEvents.ts
src/components/canvas/GraphComponent/index.tsx
src/components/canvas/layers/connectionLayer/ConnectionLayer.ts
src/components/canvas/layers/selectionLayer/SelectionLayer.ts
src/components/canvas/layers/newBlockLayer/NewBlockLayer.ts
src/plugins/devtools/DevToolsLayer.ts
src/plugins/minimap/layer.ts
src/components/canvas/blocks/Block.ts
src/components/canvas/connections/BaseConnection.ts
src/components/canvas/anchors/index.ts
src/components/canvas/groups/Group.ts
src/graph.ts
Refactored dragListener and utility functions to handle PointerEvent
  • Replaced dragListener DOM events (mousemove/mousedown/mouseup/mouseleave) with pointer equivalents
  • Updated getXY, getCoord to accept PointerEvent and simplified coordinate retrieval
  • Extended isMetaKeyEvent, isShiftKeyEvent, isAltKeyEvent, getEventSelectionAction and addEventListeners to include PointerEvent
  • Adjusted createCustomDragEvent to compute coordinates via getCoord
src/utils/functions/dragListener.ts
src/utils/functions/index.ts
Refactored BlockController to extract pointer-down drag logic
  • Introduced standalone handlePointerDownEvent helper for drag-and-drop setup
  • Replaced inline mousedown listener with pointerdown referencing the helper
  • Removed duplicated dragListener code in constructor
src/components/canvas/blocks/controllers/BlockController.ts
Updated CSS to improve touch support
  • Added touch-action: none to block, anchor, and layer styles
  • Added -webkit-tap-highlight-color: transparent to Layer.css
src/react-components/Block.css
src/react-components/Anchor.css
src/services/Layer.css

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @pavel-klimov - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/components/canvas/layers/graphLayer/GraphLayer.ts:29` </location>
<code_context>
-  "mouseup",
-  "touchend",
+  "pointerdown",
+  "pointerenter",
+  "pointermove",
   "click",
   "dblclick",
</code_context>

<issue_to_address>
Including 'pointerenter' and 'pointermove' as bubbling events may not match their actual event propagation.

'pointerenter' and 'pointerleave' do not bubble, so including them here may cause issues. Consider handling them separately or only during the capturing phase.
</issue_to_address>

### Comment 2
<location> `src/components/canvas/layers/graphLayer/GraphLayer.ts:315` </location>
<code_context>
-
-    if (this.canEmulateClick && (event.type === "click" || event.type === "dblclick")) {
-      this.applyEventToTargetComponent(new MouseEvent(event.type, event), this.pointerStartTarget);
+      this.applyEventToTargetComponent(new PointerEvent(event.type, event), target);
     }
   }
</code_context>

<issue_to_address>
Reconstructing a PointerEvent from an existing event may lose some properties.

Only properties included in eventInitDict are copied, so some original event properties may be lost. Consider re-dispatching the original event or using a more thorough cloning method if full fidelity is required.
</issue_to_address>

### Comment 3
<location> `src/components/canvas/blocks/controllers/BlockController.ts:17` </location>
<code_context>
 import { ESelectionStrategy } from "../../../../utils/types/types";
 import { Block } from "../Block";

+const handlePointerDownEvent = (event: PointerEvent, block: Block, self: any) => {
+  const blockState = selectBlockById(block.context.graph, block.props.id);
+  const allowChangeBlockGeometry = isAllowChangeBlockGeometry(
</code_context>

<issue_to_address>
Consider moving the event handler into the class as a private method to avoid passing extra parameters and centralize logic.

```suggestion
Rather than a top‐level `handlePointerDownEvent(event, block, self)`, you can simplify by moving it into the class as a private arrow or bound method. This removes the extra parameters (`block`/`self`) and keeps all logic in one place:

```ts
export class BlockController {
  private block: Block;

  constructor(block: Block) {
    this.block = block;
    addEventListeners(block as EventTarget, {
      click: this.onClick,
      pointerdown: this.onPointerDown,   // ← use the private method directly
    });
  }

  private onClick = (event: PointerEvent) => {
    event.stopPropagation();
    const { connectionsList } = this.block.context.graph.rootStore;
    if (!isMetaKeyEvent(event) && connectionsList.$selectedConnections.value.size) {
      connectionsList.resetSelection();
    }
    this.block.context.graph.api.selectBlocks(
      [this.block.props.id],
      !isMetaKeyEvent(event),
      !isMetaKeyEvent(event) ? ESelectionStrategy.REPLACE : ESelectionStrategy.APPEND,
    );
  };

  private onPointerDown = (event: PointerEvent) => {
    const blk = this.block;
    const graph = blk.context.graph;
    const state = selectBlockById(graph, blk.props.id);
    const allow = isAllowChangeBlockGeometry(
      blk.getConfigFlag("canChangeBlockGeometry") as ECanChangeBlockGeometry,
      state.selected
    );
    if (!allow) return;

    event.stopPropagation();
    const store = graph.rootStore.blocksList;
    const selectedStates = getSelectedBlocks(state, store);
    const components = selectedStates.map(s => s.getViewComponent());

    dragListener(blk.context.ownerDocument)
      .on(EVENTS.DRAG_START, e => {
        graph.getGraphLayer().captureEvents(this);
        dispatchEvents(components, createCustomDragEvent(EVENTS.DRAG_START, e));
      })
      .on(EVENTS.DRAG_UPDATE, e => {
        dispatchEvents(components, createCustomDragEvent(EVENTS.DRAG_UPDATE, e));
      })
      .on(EVENTS.DRAG_END, e => {
        graph.getGraphLayer().releaseCapture();
        dispatchEvents(components, createCustomDragEvent(EVENTS.DRAG_END, e));
      });
  };
}
```

Benefits:
- No need to pass `block` or `this` around.
- All event logic lives inside the controller.
- Easier to follow control flow and keep member access via `this.block`.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gravity-ui-bot
Copy link
Contributor

Preview is ready.

@draedful draedful self-assigned this Jul 10, 2025
@pavel-klimov pavel-klimov force-pushed the pavelklimov/fixed-mobile branch from bf4b1ce to c676502 Compare July 10, 2025 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants