Graphora API
    Preparing search index...

    Module @graphora/interactions

    Interactions Package

    Package name: @graphora/interactions

    Planned responsibility: pan, zoom, hover, click, drag, selection, keyboard handling, and future editing behaviors.

    attachGraphInput(container, engine, renderer) adds primary-pointer hover, node/edge selection, node dragging, background panning and wheel zoom to a mounted vanilla graph. Call its returned disposer before destroying the engine. It uses a structural host interface and has no engine or React dependency. Optional onClick and onDragEnd callbacks support app-owned status UI.

    See interaction concepts and the vanilla interaction example. Keyboard controls, menus and tooltips remain app-owned.

    Highlight is immutable visual emphasis independent from hover, focus, selection, and drag. createHighlightState, replaceHighlight, and clearHighlight manage explicit node and edge IDs.

    highlightNeighborhood(snapshot, { nodeId, mode }) derives a depth-one neighborhood from a public graph snapshot. direct and all include inbound and outbound relations; inbound and outbound respect directed edges. highlightToInteractionRenderState copies only highlight IDs into an existing renderer interaction state, preserving its other fields.

    The first interaction primitive is framework-agnostic hover state. It converts renderer pick results into immutable hover state without owning selection, focus, drag, DOM listeners, or React behavior.

    import { createHoverState, updateHoverFromPick } from '@graphora/interactions'

    let hover = createHoverState()
    const update = updateHoverFromPick(hover, renderer.pick(point, snapshot))

    if (update.changed) {
    hover = update.state
    }

    Hover state tracks node and edge targets separately:

    • hoveredNodeId
    • hoveredEdgeId

    Canvas and miss picks clear hover. Selection, focus, click handling, dragging, and pointer listener lifecycle are intentionally left to later interaction tasks.

    Selection state is also framework-agnostic and stores node and edge IDs separately. It does not require edge picking to be implemented, and it does not own click handling, keyboard shortcuts, selected styling, hover, focus, or drag state.

    import {
    createSelectionState,
    selectItem,
    toggleSelection
    } from '@graphora/interactions'

    let selection = createSelectionState()
    selection = selectItem(selection, { type: 'node', nodeId: 'a' }).state
    selection = toggleSelection(selection, { type: 'edge', edgeId: 'ab' }).state

    Selection helpers return { state, previousState, changed }, so integrations can skip redraws for idempotent operations. Selected IDs are duplicate-free, in deterministic insertion order, and frozen.

    Use selectionToInteractionRenderState(selection) when a renderer snapshot needs the selected node and edge arrays. Visual selected styling is still owned by later styling work.

    Node dragging is represented as a pure drag session. It tracks one active node, an optional pointer ID, start/current screen points, and the current node position in world coordinates.

    import {
    applyNodeDragPosition,
    startNodeDrag,
    updateNodeDrag
    } from '@graphora/interactions'

    let drag = startNodeDrag({
    nodeId: 'a',
    pointerId: 1,
    screenPoint: { x: 100, y: 100 },
    nodePosition: { x: 0, y: 0 },
    viewport
    }).state

    drag = updateNodeDrag(drag, {
    pointerId: 1,
    screenPoint: { x: 120, y: 100 },
    viewport
    }).state

    applyNodeDragPosition(store, drag)

    Screen-space movement is divided by the viewport zoom, so a 20px pointer move at zoom 2 moves the node by 10 world units. Applying a drag uses GraphStore.updateNodePosition, preserving core position-update and graph:changed events.

    This package does not yet attach DOM pointer listeners or call setPointerCapture; integrations can use the stored pointer ID as the framework-agnostic capture token until pointer lifecycle work lands.

    Pan and zoom controls wrap the renderer viewport utilities with small interaction-state helpers. They do not attach DOM event listeners.

    import {
    startPan,
    updatePan,
    zoomViewportByWheel
    } from '@graphora/interactions'

    let pan = startPan({
    pointerId: 1,
    screenPoint: { x: 120, y: 80 },
    viewport
    }).state

    const nextPan = updatePan(pan, {
    pointerId: 1,
    screenPoint: { x: 140, y: 80 }
    })

    const nextZoom = zoomViewportByWheel(nextPan.viewport!, {
    screenPoint: { x: 200, y: 120 },
    deltaY: -100
    })

    updatePan uses the viewport from pan start plus the current screen delta, so the visible graph follows the pointer according to the established viewport panBy convention. zoomViewportByWheel delegates to zoomAt, preserving the world point under the cursor while clamping to the viewport zoom range.

    Use fitViewportToBounds and fitViewportToNode for graph and item fitting.

    Lasso selection uses the same pure-state style as pan, zoom, hover, drag, and click helpers. It tracks one active pointer, rectangle or polygon geometry in screen space, and the matching world-space bounds/points used for node hit testing.

    import {
    applyLassoSelection,
    nodeIdsInLasso,
    startLasso,
    updateLasso
    } from '@graphora/interactions'

    let lasso = startLasso({
    pointerId: 1,
    screenPoint: { x: 120, y: 80 },
    viewport
    }).state

    lasso = updateLasso(lasso, {
    pointerId: 1,
    screenPoint: { x: 220, y: 160 },
    viewport
    }).state

    const selectedNodeIds = nodeIdsInLasso(lasso, renderNodes)
    selection = applyLassoSelection(selection, selectedNodeIds).state

    Integrations can draw active feedback from screenBounds for rectangle mode or screenPoints for polygon mode. The helpers intentionally avoid DOM listeners and renderer overlay ownership, so lasso can compose with app-specific pointer capture and canvas/SVG/HTML feedback layers.

    Lasso applies only to nodes for now and preserves selected edge IDs. Edge selection behavior is explicitly deferred until edge hit-testing and selection semantics are expanded.

    Click helpers convert renderer pick results into framework-agnostic payloads for node, edge, and canvas clicks. They also keep click-vs-drag threshold logic out of DOM-specific code.

    import {
    clickEventFromPick,
    focusViewportOnNode,
    isClickWithinDragThreshold
    } from '@graphora/interactions'

    if (isClickWithinDragThreshold(pointerDownPoint, pointerUpPoint)) {
    const event = clickEventFromPick(renderer.pick(pointerUpPoint), viewport)
    }

    const focused = focusViewportOnNode(viewport, {
    position: { x: 20, y: -10 },
    radius: 16
    })

    clickEventFromPick returns node:clicked, edge:clicked, or canvas:clicked payloads with screen point, world point, and viewport context. Miss or null picks return null. DOM listener wiring and React callbacks are left to later layers.

    Context-menu helpers reuse renderer pick results so vanilla, React, and other integrations can render menus without duplicating graph hit testing. The interactions package only creates the intent payload; the app owns the menu DOM, commands, focus management, and whether to suppress the browser's default context menu.

    import { contextMenuEventFromPick } from '@graphora/interactions'

    container.addEventListener('contextmenu', (event) => {
    event.preventDefault()

    const point = screenPointFromEvent(event)
    const menu = contextMenuEventFromPick(
    renderer.pick(point),
    engine.getViewport(),
    { trigger: 'pointer' }
    )

    if (menu?.type === 'node:context-menu') {
    showNodeMenu(menu.nodeId, menu.screenPoint)
    }
    })

    contextMenuEventFromPick returns node:context-menu, edge:context-menu, or canvas:context-menu payloads with screen point, world point, viewport context, and a trigger of pointer, keyboard, or programmatic. Miss or null picks return null.

    Keyboard integrations should provide the screen point where the menu should open, such as the focused item center or the viewport center, then pass { trigger: 'keyboard' }. Keep preventDefault() in the integration layer so apps can choose where native browser context menus remain available.

    Tooltip helpers reuse renderer pick results to create framework-agnostic tooltip intent payloads. The package does not render tooltip DOM, manage async content, or own hover/focus/click listener timing; integrations decide those details.

    import { tooltipEventFromPick } from '@graphora/interactions'

    container.addEventListener('pointermove', (event) => {
    const point = screenPointFromEvent(event)
    const tooltip = tooltipEventFromPick(
    renderer.pick(point),
    engine.getViewport(),
    { trigger: 'hover', showDelayMs: 120, hideDelayMs: 80 }
    )

    if (tooltip?.type === 'node:tooltip') {
    showTooltip(tooltip.nodeId, tooltip.screenPoint)
    }
    })

    tooltipEventFromPick returns node:tooltip, edge:tooltip, or canvas:tooltip payloads with screen point, world point, viewport context, trigger, and optional show/hide delay metadata. Miss or null picks return null.

    Supported triggers are hover, focus, click, and programmatic. Async content remains application-owned: keep pending requests, loading states, and DOM or framework rendering outside @graphora/interactions.

    createFocusState, moveFocus, reconcileFocus, clearFocus and focusToInteractionRenderState manage one node/edge target independently of selection, hover, highlight and DOM focus. moveFocus wraps in the supplied visible order; reconcileFocus clears removed/filtered targets. IDs compare by String(id) within separate node/edge namespaces. Always reconcile after changing visible data, even when the next key has not arrived yet.

    let focus = createFocusState()
    focus = moveFocus(focus, [{ type: 'node', nodeId: 'a' }])
    engine.setInteractionState(
    focusToInteractionRenderState(focus, engine.getInteractionState())
    )
    const shortcut = matchKeyboardShortcut(
    { key: 'ArrowRight', editable: false },
    DEFAULT_GRAPH_SHORTCUTS
    )
    // Application executes shortcut?.command and decides whether to preventDefault.

    Import these helpers from @graphora/interactions. The matcher accepts normalized input, never a required DOM event. It rejects editable, composing and already handled events, and requires exact key/modifier matches. Apps inspect composed paths/ancestors for form controls, contenteditable regions and custom editors; pass the result as editable. Matching is disabled by enabled: false or an empty binding list. Only allowEditable: true overrides editability suppression. Defaults are optional; Tab is unbound and shortcuts never install global listeners.

    The vanilla interaction and React callbacks examples share app-owned wiring in examples/shared/keyboard-demo.ts: node/edge navigation, candidate-ID filtering, selection toggling, fit focused/selected items, and independent focus clearing. They remap node-navigation defaults to item-navigation commands. Edge fitting uses positioned endpoints. Canvas remains a visual surface; applications own semantic companion UI and screen-reader validation.

    CanvasClickEvent
    CanvasContextMenuEvent
    CanvasTooltipEvent
    ClickEventBase
    ContextMenuEventBase
    ContextMenuOptions
    EdgeClickEvent
    EdgeContextMenuEvent
    EdgeTooltipEvent
    FitNodeInput
    GraphInputHost
    HighlightState
    HighlightStateInput
    HoverState
    HoverStateUpdate
    KeyboardInput
    KeyboardMatchOptions
    KeyboardShortcut
    LassoEndInput
    LassoSelectableNode
    LassoSelectionOptions
    LassoStartInput
    LassoState
    LassoStateUpdate
    LassoUpdateInput
    NeighborhoodHighlightInput
    NodeClickEvent
    NodeContextMenuEvent
    NodeDragEndInput
    NodeDragStartInput
    NodeDragState
    NodeDragStateUpdate
    NodeDragUpdateInput
    NodeTooltipEvent
    PanEndInput
    PanStartInput
    PanState
    PanStateUpdate
    PanUpdateInput
    SelectionState
    SelectionStateInput
    SelectionStateUpdate
    TooltipEventBase
    TooltipOptions
    ViewportControlUpdate
    WheelZoomInput
    ContextMenuTrigger
    DragPointerId
    FocusTarget
    GraphClickEvent
    GraphContextMenuEvent
    GraphKeyboardCommand
    GraphTooltipEvent
    HighlightSource
    HoverTarget
    LassoMode
    LassoPointerId
    LassoSelectionMode
    NeighborhoodHighlightMode
    PanPointerId
    SelectionTarget
    TooltipTrigger
    DEFAULT_GRAPH_SHORTCUTS
    DEFAULT_HOVER_STATE
    DEFAULT_LASSO_STATE
    DEFAULT_NODE_DRAG_STATE
    DEFAULT_PAN_STATE
    DEFAULT_SELECTION_STATE
    interactionsPackageDependencies
    interactionsPackageInfo
    interactionsPackageName
    applyLassoSelection
    applyNodeDragPosition
    attachGraphInput
    cancelLasso
    cancelNodeDrag
    cancelPan
    clearFocus
    clearHighlight
    clearHover
    clearSelection
    clickEventFromPick
    contextMenuEventFromPick
    createFocusState
    createHighlightState
    createHoverState
    createSelectionState
    deselectItem
    dragToInteractionRenderState
    endLasso
    endNodeDrag
    endPan
    fitViewportToBounds
    fitViewportToNode
    focusToInteractionRenderState
    focusViewportOnNode
    focusViewportOnNodes
    highlightExplicitTargets
    highlightNeighborhood
    highlightToInteractionRenderState
    hoverTargetFromPick
    isClickWithinDragThreshold
    matchKeyboardShortcut
    moveFocus
    nodeIdsInLasso
    reconcileFocus
    replaceHighlight
    replaceSelection
    selectionToInteractionRenderState
    selectItem
    startLasso
    startNodeDrag
    startPan
    toggleSelection
    tooltipEventFromPick
    updateHover
    updateHoverFromPick
    updateLasso
    updateNodeDrag
    updatePan
    zoomViewportByWheel