Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-snap-center-to-cursor-modifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/dom': patch
---

Added `SnapToPointer` modifier that offsets the drag transform so a specified anchor point of the dragged element snaps to the cursor position. The `anchor` option accepts an `{x, y}` object with values between `0` and `1` representing the relative position within the draggable element. Defaults to `{x: 0.5, y: 0.5}` (center).
77 changes: 50 additions & 27 deletions apps/docs/extend/modifiers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Available in `@dnd-kit/abstract/modifiers`, these modifiers are environment-agno
<Card title="RestrictToVerticalAxis">
Constrain movement to the vertical axis only
</Card>
<Card title="Snap">
<Card title="SnapToPointer">
Snap movement to a grid with configurable size
</Card>
</CardGroup>
Expand All @@ -40,21 +40,18 @@ const manager = new DragDropManager({
Example combining modifiers:

```ts
import {
Snap,
RestrictToHorizontalAxis
} from '@dnd-kit/abstract/modifiers';
import {Snap, RestrictToHorizontalAxis} from '@dnd-kit/abstract/modifiers';

// Horizontal movement that snaps to a grid
const manager = new DragDropManager({
modifiers: [
RestrictToHorizontalAxis,
Snap.configure({
size: {
x: 20, // Snap every 20px horizontally
y: 0 // No vertical snapping (already restricted)
}
})
x: 20, // Snap every 20px horizontally
y: 0, // No vertical snapping (already restricted)
},
}),
],
});
```
Expand All @@ -74,6 +71,10 @@ Environment-specific modifiers for the DOM, available in `@dnd-kit/dom/modifiers
<Card title="RestrictToElement">
Constrain movement within a container element
</Card>
<Card title="SnapToPointer">
Offset the drag transform so a specified anchor point of the element snaps
to the cursor position
</Card>
</CardGroup>

## Usage
Expand All @@ -82,28 +83,50 @@ Modifiers can be applied globally or per draggable element:

```ts
import {DragDropManager} from '@dnd-kit/dom';
import {
RestrictToWindow,
RestrictToElement
} from '@dnd-kit/dom/modifiers';
import {RestrictToWindow, RestrictToElement} from '@dnd-kit/dom/modifiers';

// Global modifiers
const manager = new DragDropManager({
modifiers: [
RestrictToWindow,
],
modifiers: [RestrictToWindow],
});

// Per-draggable modifiers
const draggable = new Draggable({
id: 'draggable-1',
element,
modifiers: [
RestrictToElement.configure({
element: containerElement
})
],
}, manager);
const draggable = new Draggable(
{
id: 'draggable-1',
element,
modifiers: [
RestrictToElement.configure({
element: containerElement,
}),
],
},
manager
);
```

### SnapToPointer

When dragging starts, `SnapToPointer` immediately repositions the element so the specified anchor point aligns with the cursor. The `anchor` option accepts an `{x, y}` object with values between `0` and `1` representing the relative position within the draggable element. Defaults to `{x: 0.5, y: 0.5}` (center).

```ts
import {DragDropManager} from '@dnd-kit/dom';
import {SnapToPointer} from '@dnd-kit/dom/modifiers';

// Default: snap center to cursor
const manager = new DragDropManager({
modifiers: [SnapToPointer],
});

// Snap top-left corner to cursor
const manager = new DragDropManager({
modifiers: [SnapToPointer.configure({anchor: {x: 0, y: 0}})],
});

// Snap bottom-right corner to cursor
const manager = new DragDropManager({
modifiers: [SnapToPointer.configure({anchor: {x: 1, y: 1}})],
});
```

<Info>
Expand Down Expand Up @@ -181,10 +204,10 @@ class SnapToGrid extends Modifier {

```ts
const snapToGrid = SnapToGrid.configure({
gridSize: 10
gridSize: 10,
});

const manager = new DragDropManager({
modifiers: [snapToGrid]
modifiers: [snapToGrid],
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
RestrictToHorizontalAxis,
RestrictToVerticalAxis,
} from '@dnd-kit/abstract/modifiers';
import {RestrictToElement, RestrictToWindow} from '@dnd-kit/dom/modifiers';
import {
RestrictToElement,
RestrictToWindow,
SnapToPointer,
} from '@dnd-kit/dom/modifiers';

import docs from './docs/ModifierDocs.mdx';
import {DraggableExample} from '../DraggableExample';
Expand Down Expand Up @@ -69,3 +73,10 @@ export const SnapModifierExample: Story = {
name: 'Snap to grid',
render: SnapToGridExample,
};

export const SnapToPointerModifier: Story = {
name: 'Snap to pointer',
args: {
modifiers: [SnapToPointer],
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {RestrictToElement} from '@dnd-kit/dom/modifiers';
import {RestrictToElement, SnapToPointer} from '@dnd-kit/dom/modifiers';

import {Info, Preview} from '../../../../components';
import {DraggableExample} from '../../DraggableExample';
Expand Down Expand Up @@ -26,6 +26,44 @@ Modifiers let you dynamically modify the movement coordinates that are detected
- Restricting motion to the draggable node's scroll container bounding rectangle
- Applying resistance or clamping the motion

## Built-in modifiers

### `@dnd-kit/abstract/modifiers`

| Modifier | Description |
| -------------------------- | ---------------------------------------------- |
| `RestrictToVerticalAxis` | Restricts drag movement to the vertical axis |
| `RestrictToHorizontalAxis` | Restricts drag movement to the horizontal axis |
| `SnapModifier` | Snaps drag movement to a grid |

### `@dnd-kit/dom/modifiers`

| Modifier | Description |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `RestrictToWindow` | Restricts the draggable element to stay within the viewport |
| `RestrictToElement` | Restricts the draggable element to stay within a given element |
| `SnapToPointer` | Offsets the drag transform so a specified anchor point of the element snaps to the cursor position |

#### `SnapToPointer`

When dragging starts, `SnapToPointer` immediately repositions the element so the specified anchor point aligns with the cursor. The `anchor` option accepts an `{x, y}` object with values between `0` and `1` representing the relative position within the draggable element. Defaults to `{x: 0.5, y: 0.5}` (center).

<Preview>
<DraggableExample modifiers={[SnapToPointer]} />
</Preview>

```jsx
import {useDraggable} from '@dnd-kit/react';
import {SnapToPointer} from '@dnd-kit/dom/modifiers';

function Draggable({id}) {
const {ref} = useDraggable({
id,
modifiers: [SnapToPointer],
});
}
```

## Usage

Modifiers can be applied globally or to individual draggable elements.
Expand Down
2 changes: 1 addition & 1 deletion packages/abstract/src/core/plugins/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export abstract class Plugin<
* @param callback - The effect callback to register
* @returns A function to dispose of the effect
*/
protected registerEffect(callback: () => void) {
protected registerEffect(callback: () => void): CleanupFunction {
const dispose = effect(callback.bind(this));

this.#cleanupFunctions.add(dispose);
Expand Down
2 changes: 1 addition & 1 deletion packages/dom/src/core/sensors/keyboard/KeyboardSensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class KeyboardSensor extends Sensor<

protected listeners = new Listeners();

public bind(source: Draggable, options = this.options) {
public bind(source: Draggable, options = this.options): CleanupFunction {
const unbind = effect(() => {
const target = source.handle ?? source.element;
const listener: EventListener = (event: Event) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/dom/src/core/sensors/pointer/PointerSensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class PointerSensor extends Sensor<
return constraints;
}

public bind(source: Draggable, options = this.options) {
public bind(source: Draggable, options = this.options): CleanupFunction {
const unbind = effect(() => {
const controller = new AbortController();
const {signal} = controller;
Expand Down
35 changes: 35 additions & 0 deletions packages/dom/src/modifiers/SnapToPointer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Modifier, configurator} from '@dnd-kit/abstract';
import type {DragOperation} from '@dnd-kit/abstract';
import type {DragDropManager} from '@dnd-kit/dom';

interface Anchor {
x: number;
y: number;
}

interface Options {
anchor?: Anchor;
}

const DEFAULT_ANCHOR: Anchor = {x: 0.5, y: 0.5};

export class SnapToPointer extends Modifier<DragDropManager, Options> {
apply({activatorEvent, shape, transform}: DragOperation) {
if (!shape || !(activatorEvent instanceof PointerEvent)) {
return transform;
}

const anchor = this.options?.anchor ?? DEFAULT_ANCHOR;
const {boundingRectangle} = shape.initial;

const anchorX = boundingRectangle.left + boundingRectangle.width * anchor.x;
const anchorY = boundingRectangle.top + boundingRectangle.height * anchor.y;

return {
x: transform.x + activatorEvent.clientX - anchorX,
y: transform.y + activatorEvent.clientY - anchorY,
};
}

static configure = configurator(SnapToPointer);
}
1 change: 1 addition & 0 deletions packages/dom/src/modifiers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {RestrictToWindow} from './RestrictToWindow.ts';
export {RestrictToElement} from './RestrictToElement.ts';
export {SnapToPointer} from './SnapToPointer.ts';