aidenybai / aidenybai/react-grab

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

Ouverte
#616 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
TypeScript
Étoiles
7.6k
Forks
340
Merge moyen
5 h 12 min
PR mergées (30 j)
6

Description

## 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:

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

const onElementSelect = (element: Element): Promise => {
  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:

```ts
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;
}
```

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:

```ts
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:

```ts
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.

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.