Feature Request: Add onSelectionCommit — one interceptable hook per committed selection gesture

Author: Geuni620Created Aug 9, 2026Updated Sep 1, 2026

Problem

As of 0.2.0, plugins still don't have a single interceptable boundary for a committed selection gesture.

The existing hooks expose different pieces of the lifecycle:

  • onElementSelect(element) fires once per element. Drag-selecting 3 elements produces 3 calls, so integrations have to reconstruct the gesture boundary themselves.
  • No selection hook exposes the pointer position that committed the selection. Plugins that need to anchor UI at the commit point have to keep their own pointer listener running.
  • onDragEnd(elements, bounds) exposes the committed group, but it is notification-only: it cannot intercept the default clipboard / Copied feedback path.

My current workaround reconstructs the commit boundary by batching onElementSelect calls:

typescript
let pendingBatch: {
  elements: Element[];
  resolvers: Array<(handled: boolean) => void>;
} | null = null;

const onElementSelect = (element: Element): Promise<boolean> => {
  if (!pendingBatch) {
    pendingBatch = { elements: [], resolvers: [] };
    queueMicrotask(flushBatch);
  }

  const batch = pendingBatch;

  if (!batch.elements.includes(element)) {
    batch.elements.push(element);
  }

  return new Promise((resolve) => {
    batch.resolvers.push(resolve);
  });
};

The integration also keeps separate pointer tracking so it can recover the position associated with that commit.

Proposal

Add one async, interceptable hook that fires once for each committed selection gesture:

typescript
interface SelectionCommit {
  readonly elements: readonly Element[];
  readonly point: Position; // pointer/client position at commit time
  readonly kind: "click" | "drag";
  readonly additive: boolean;
  readonly bounds: DragRect | null; // non-null for drag commits
}

interface PluginHooks {
  onSelectionCommit?: (
    selection: SelectionCommit,
  ) => boolean | void | Promise<boolean | void>;
}

Semantics:

  • Invoke it once per committed gesture, regardless of the number of selected elements.
  • Resolving true means the plugin handled the selection — skip React Grab's built-in clipboard write and Copied feedback.
  • false / undefined preserves the existing copy behavior.
  • additive indicates that the gesture extended the existing selection, e.g. Shift + click or Shift + drag.
  • For keyboard-committed selections, point falls back to the center of the target element.
  • onElementSelect and onDragEnd stay unchanged for backward compatibility.

Why a commit-level hook?

The plugin API already exposes most of the required information, but at different lifecycle boundaries:

typescript
onElementSelect(element)
onDragEnd(elements, bounds)

The missing piece is a single interception point representing:

"This selection gesture has now committed these elements."

For pointer-driven selections, React Grab already has the selected elements, gesture type, pointer position, and drag bounds while processing the commit.

This would keep gesture reconstruction inside the component that already owns selection state rather than requiring every integration to rebuild it independently.

Plugin use cases

A commit-level boundary would also make higher-level workflows implementable outside core as plugins, for example:

  • accumulated selections / "Copy list" workflows (#317)
  • the former comments dropdown / Copy All workflow (#421)
  • comment + element accumulation workflows such as #634

Those plugins could consume the committed selection and own their storage/UI/export behavior without requiring those workflows to live in React Grab core.

Current integration cost

My integration currently has roughly ~200 lines across batching, resolver fan-out, pointer tracking, and tests to reconstruct this boundary.

With this hook, the integration becomes roughly:

typescript
hooks: {
  onSelectionCommit: async (selection) => {
    await annotations.add(selection);
    return true;
  },
}

Related: #340 (multi-select, shipped) · #421 · #317 · #634

Happy to submit a PR if this API direction sounds useful.