aidenybai / aidenybai/react-grab

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

未关闭
#616 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
TypeScript
星标
7.6k
派生
340
平均合并
5 小时 12 分钟
30 天内合并 PR
6

描述

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

贡献指南

打开贡献指南

调研方向

Start by finding the plugin hook definitions for onElementSelect and onDragEnd, then trace where selection gestures commit for click, drag, and keyboard paths. The proposal defines the desired SelectionCommit shape and behavior; done means onSelectionCommit fires once per committed gesture, can skip built-in copy feedback when it returns true, and existing hooks remain compatible.

由索引模型根据 Issue 内容生成。

评估

技术栈
react, typescript
领域
api, frontend
Issue 类型
功能
难度
5/5
预计耗时
一周以上
活跃度
冷清
描述清晰度
基本清楚
新手友好度
35/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。