- {lineNumbers.map((line) => (
-
- {line}
-
-
- ))}
+ {lineCount > 1
+ ? createRange(lineCount).map((line) => (
+
+ {line + 1}
+
+
+ ))
+ : null}
@@ -72,5 +84,3 @@ function syntaxReplacements(value: string) {
value
);
}
-
-const lineNumbers = createRange(50);
diff --git a/apps/docs/stories/react/components/Item/Item.module.css b/apps/docs/stories/react/components/Item/Item.module.css
index c479fdcf..729ca3db 100644
--- a/apps/docs/stories/react/components/Item/Item.module.css
+++ b/apps/docs/stories/react/components/Item/Item.module.css
@@ -11,17 +11,15 @@
background-color: #FFFFFF;
border-radius: 8px;
font-size: 14px !important;
- font-weight: 600;
- color: #333333;
- cursor: grab;
+ color: #555;
outline: none;
transition: background 0.4s ease, box-shadow 0.3s ease, transform 0.25s ease;
min-height: 62px;
box-shadow: var(--box-shadow);
- font-family: sans-serif !important;
- font-weight: 300;
+ font-family: var(--font-family);
width: 100%;
max-width: 300px;
+ white-space: nowrap;
}
.Item:focus-visible:not(.shadow) {
@@ -40,6 +38,7 @@
}
.Item:not(.hasActions) {
+ cursor: grab;
touch-action: none;
}
diff --git a/apps/docs/stories/react/components/Preview/Preview.module.css b/apps/docs/stories/react/components/Preview/Preview.module.css
index f788818a..6824d27c 100644
--- a/apps/docs/stories/react/components/Preview/Preview.module.css
+++ b/apps/docs/stories/react/components/Preview/Preview.module.css
@@ -1,10 +1,36 @@
.Preview {
- max-height: 400px;
overflow-y: auto;
- padding: 25px;
+ padding: 30px 40px;
margin: 25px 0 40px;
- border-radius: 4px;
- background: #FFFFFF;
- box-shadow: rgba(0, 0, 0, 0.10) 0 1px 3px 0;
- border: 1px solid hsla(203, 50%, 30%, 0.15);
+ border-radius: 8px;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.10), inset 0 0 1px hsla(203, 50%, 30%, 0.5);
+ background: #FCFCFC;
+}
+
+.Preview:has( + .Code) {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ margin-bottom: 0;
+}
+
+.Preview + .Code > div > div {
+ margin-top: 0px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.hero {
+ max-height: 400px;
+ background: linear-gradient(65deg, #56fff410, #001AFF10, #5F6AF210, #F25FD010, #56FFF510, #F25FD010, #001AFF10, #56fff410);
+ animation: gradient 16s linear infinite;
+ animation-direction: alternate;
+ background-size: 600% 100%;
+}
+
+
+
+@keyframes gradient {
+ 0% {background-position: 0%}
+ 100% {background-position: 100%}
}
+
diff --git a/apps/docs/stories/react/components/Preview/Preview.tsx b/apps/docs/stories/react/components/Preview/Preview.tsx
index c45497cf..9ba5741b 100644
--- a/apps/docs/stories/react/components/Preview/Preview.tsx
+++ b/apps/docs/stories/react/components/Preview/Preview.tsx
@@ -2,17 +2,29 @@ import React from 'react';
import {Story, Unstyled} from '@storybook/blocks';
import {type StoryFn} from '@storybook/react';
+import {classNames} from '../../../utilities';
+import {Code} from '../Code';
import styles from './Preview.module.css';
interface Props {
of?: StoryFn;
+ code?: string;
+ hero?: boolean;
+ tabs?: string[];
children?: React.ReactNode;
}
-export function Preview({children, of}: Props) {
+export function Preview({children, code, of, hero, tabs}: Props) {
return (
- {children ?? }
+
+ {children ?? }
+
+ {code ? (
+
+ {code}
+
+ ) : null}
);
}
diff --git a/packages/abstract/src/collision/index.ts b/packages/abstract/src/collision/index.ts
index 03d6fda7..36622f7f 100644
--- a/packages/abstract/src/collision/index.ts
+++ b/packages/abstract/src/collision/index.ts
@@ -1,3 +1,4 @@
export {CollisionObserver} from './observer';
export {CollisionNotifier} from './notifier';
+export {sortCollisions} from './utilities';
export * from './types';
diff --git a/packages/abstract/src/collision/notifier.ts b/packages/abstract/src/collision/notifier.ts
index 648c0f80..3901ad44 100644
--- a/packages/abstract/src/collision/notifier.ts
+++ b/packages/abstract/src/collision/notifier.ts
@@ -1,4 +1,4 @@
-import {effect} from '@dnd-kit/state';
+import {effect, untracked} from '@dnd-kit/state';
import {DragDropManager} from '../manager';
import {Plugin} from '../plugins';
@@ -10,10 +10,18 @@ export class CollisionNotifier extends Plugin {
this.destroy = effect(() => {
const {collisionObserver, monitor} = manager;
const {collisions} = collisionObserver;
+
+ if (collisionObserver.isDisabled()) {
+ return;
+ }
+
let defaultPrevented = false;
monitor.dispatch('collision', {
collisions,
+ get defaultPrevented() {
+ return defaultPrevented;
+ },
preventDefault() {
defaultPrevented = true;
},
diff --git a/packages/abstract/src/collision/observer.ts b/packages/abstract/src/collision/observer.ts
index 6f7a8f71..ffe7ac1f 100644
--- a/packages/abstract/src/collision/observer.ts
+++ b/packages/abstract/src/collision/observer.ts
@@ -1,60 +1,63 @@
import {computed, ReadonlySignal} from '@dnd-kit/state';
import {isEqual} from '@dnd-kit/utilities';
-import type {DragOperation, DragDropRegistry} from '../manager';
+import type {DragDropManager} from '../manager';
import type {Draggable, Droppable} from '../nodes';
-import type {Collision, Collisions} from './types';
+import {Plugin} from '../plugins';
+import type {Collision, CollisionDetector, Collisions} from './types';
import {sortCollisions} from './utilities';
-export type Input<
- T extends Draggable = Draggable,
- U extends Droppable = Droppable,
-> = {
- dragOperation: DragOperation
;
- registry: DragDropRegistry;
-};
-
const DEFAULT_VALUE: Collisions = [];
export class CollisionObserver<
T extends Draggable = Draggable,
U extends Droppable = Droppable,
-> {
- constructor({registry, dragOperation}: Input) {
- this.__computedCollisions = computed(() => {
- const {source, shape, initialized} = dragOperation;
+ V extends DragDropManager = DragDropManager,
+> extends Plugin {
+ constructor(manager: V) {
+ super(manager);
- if (!initialized || !shape) {
- return DEFAULT_VALUE;
- }
+ this.computeCollisions = this.computeCollisions.bind(this);
+ this.__computedCollisions = computed(this.computeCollisions, isEqual);
+ }
- const type = source?.type;
- const collisions: Collision[] = [];
+ public computeCollisions(
+ entries?: Droppable[],
+ collisionDetector?: CollisionDetector
+ ) {
+ const {registry, dragOperation} = this.manager;
+ const {source, shape, initialized} = dragOperation;
- for (const entry of registry.droppable) {
- if (entry.disabled) {
- continue;
- }
+ if (!initialized || !shape) {
+ return DEFAULT_VALUE;
+ }
- if (type != null && !entry.accepts(type)) {
- continue;
- }
+ const type = source?.type;
+ const collisions: Collision[] = [];
+
+ for (const entry of entries ?? registry.droppable) {
+ if (entry.disabled) {
+ continue;
+ }
+
+ if (type != null && !entry.accepts(type)) {
+ continue;
+ }
- const {collisionDetector} = entry;
- const collision = collisionDetector({
- droppable: entry,
- dragOperation,
- });
+ const detectCollision = collisionDetector ?? entry.collisionDetector;
+ const collision = detectCollision({
+ droppable: entry,
+ dragOperation,
+ });
- if (collision) {
- collisions.push(collision);
- }
+ if (collision) {
+ collisions.push(collision);
}
+ }
- collisions.sort(sortCollisions);
+ collisions.sort(sortCollisions);
- return collisions;
- }, isEqual);
+ return collisions;
}
public get collisions() {
diff --git a/packages/abstract/src/index.ts b/packages/abstract/src/index.ts
index 97365e13..02699de7 100644
--- a/packages/abstract/src/index.ts
+++ b/packages/abstract/src/index.ts
@@ -6,7 +6,7 @@ export type {
DragDropEvents,
} from './manager';
-export {CollisionPriority} from './collision';
+export {CollisionPriority, sortCollisions} from './collision';
export type {Collision, CollisionDetector} from './collision';
export {Modifier} from './modifiers';
diff --git a/packages/abstract/src/manager/dragOperation.ts b/packages/abstract/src/manager/dragOperation.ts
index 0ebb46bd..705591ed 100644
--- a/packages/abstract/src/manager/dragOperation.ts
+++ b/packages/abstract/src/manager/dragOperation.ts
@@ -1,7 +1,7 @@
import {Position, type Shape} from '@dnd-kit/geometry';
import type {Coordinates} from '@dnd-kit/geometry';
import type {UniqueIdentifier} from '@dnd-kit/types';
-import {batch, computed, signal} from '@dnd-kit/state';
+import {batch, effect, computed, signal} from '@dnd-kit/state';
import type {Draggable, Droppable} from '../nodes';
@@ -22,6 +22,7 @@ export interface DragOperation<
T extends Draggable = Draggable,
U extends Droppable = Droppable,
> {
+ activatorEvent: Event | null;
status: Status;
position: Position;
transform: Coordinates;
@@ -51,6 +52,7 @@ export function DragOperationManager<
const status = signal(Status.Idle);
const shape = signal(null);
const position = new Position({x: 0, y: 0});
+ const activatorEvent = signal(null);
const sourceIdentifier = signal(null);
const targetIdentifier = signal(null);
const source = computed(() => {
@@ -66,7 +68,8 @@ export function DragOperationManager<
const transform = computed(() => {
const {x, y} = position.delta;
let transform = {x, y};
- const operation = {
+ const operation: Omit, 'transform'> = {
+ activatorEvent: activatorEvent.peek(),
source: source.peek() ?? null,
target: target.peek() ?? null,
initialized: status.peek() !== Status.Idle,
@@ -83,6 +86,9 @@ export function DragOperationManager<
});
const operation: DragOperation = {
+ get activatorEvent() {
+ return activatorEvent.value;
+ },
get source() {
return source.value ?? null;
},
@@ -143,8 +149,9 @@ export function DragOperationManager<
operation: snapshot(operation),
});
},
- start({coordinates}: {coordinates: Coordinates}) {
+ start({event, coordinates}: {event: Event; coordinates: Coordinates}) {
status.value = Status.Initializing;
+ activatorEvent.value = event;
batch(() => {
status.value = Status.Dragging;
@@ -153,14 +160,46 @@ export function DragOperationManager<
monitor.dispatch('dragstart', {});
},
- move({coordinates}: {coordinates: Coordinates}) {
+ move({
+ by,
+ to,
+ cancelable = true,
+ }:
+ | {by: Coordinates; to?: undefined; cancelable?: boolean}
+ | {by?: undefined; to: Coordinates; cancelable?: boolean}) {
if (!dragging.peek()) {
return;
}
- position.update(coordinates);
+ let defaultPrevented = false;
+
+ monitor.dispatch('dragmove', {
+ operation: snapshot(operation),
+ by,
+ to,
+ cancelable,
+ get defaultPrevented() {
+ return defaultPrevented;
+ },
+ preventDefault() {
+ if (!cancelable) {
+ return;
+ }
+
+ defaultPrevented = true;
+ },
+ });
- monitor.dispatch('dragmove', {});
+ if (defaultPrevented) {
+ return;
+ }
+
+ const coordinates = to ?? {
+ x: position.current.x + by.x,
+ y: position.current.y + by.y,
+ };
+
+ position.update(coordinates);
},
stop({canceled = false}: {canceled?: boolean} = {}) {
const end = () => {
diff --git a/packages/abstract/src/manager/manager.ts b/packages/abstract/src/manager/manager.ts
index 9b2aa5e8..482b11d6 100644
--- a/packages/abstract/src/manager/manager.ts
+++ b/packages/abstract/src/manager/manager.ts
@@ -64,14 +64,10 @@ export class DragDropManager<
this.modifiers = new PluginRegistry>(this);
const {actions, operation} = DragOperationManager(this);
- const collisionObserver = new CollisionObserver({
- dragOperation: operation,
- registry,
- });
this.actions = actions;
- this.collisionObserver = collisionObserver;
this.dragOperation = operation;
+ this.collisionObserver = new CollisionObserver(this);
for (const modifier of modifiers) {
this.modifiers.register(modifier);
diff --git a/packages/abstract/src/manager/monitor.ts b/packages/abstract/src/manager/monitor.ts
index c623b22e..8e41c47d 100644
--- a/packages/abstract/src/manager/monitor.ts
+++ b/packages/abstract/src/manager/monitor.ts
@@ -1,4 +1,4 @@
-import type {AnyFunction} from '@dnd-kit/types';
+import type {Coordinates} from '@dnd-kit/geometry';
import type {DragDropManager} from './manager';
import type {DragOperation} from './dragOperation';
@@ -42,23 +42,31 @@ class Monitor {
}
}
-type DragDropEvent<
- T extends Draggable,
- U extends Droppable,
- V extends DragDropManager,
-> = (event: Record, manager: V) => void;
-
export type DragDropEvents<
T extends Draggable,
U extends Droppable,
V extends DragDropManager,
> = {
collision(
- event: {collisions: Collisions; preventDefault(): void},
+ event: {
+ collisions: Collisions;
+ defaultPrevented: boolean;
+ preventDefault(): void;
+ },
manager: V
): void;
dragstart(event: {}, manager: V): void;
- dragmove(event: {}, manager: V): void;
+ dragmove(
+ event: {
+ operation: DragOperation;
+ to?: Coordinates;
+ by?: Coordinates;
+ cancelable: boolean;
+ defaultPrevented: boolean;
+ preventDefault(): void;
+ },
+ manager: V
+ ): void;
dragover(event: {operation: DragOperation}, manager: V): void;
dragend(
event: {
diff --git a/packages/abstract/src/manager/registry.ts b/packages/abstract/src/manager/registry.ts
index 72564e69..655be7e7 100644
--- a/packages/abstract/src/manager/registry.ts
+++ b/packages/abstract/src/manager/registry.ts
@@ -13,27 +13,17 @@ class Registry {
}
public get(identifier: UniqueIdentifier): T | undefined {
- return this.map.peek().get(identifier);
+ return this.map.value.get(identifier);
}
- public pick(...identifiers: UniqueIdentifier[]): T[] | undefined {
- const map = this.map.peek();
-
- return identifiers.map((identifier) => {
- const entry = map.get(identifier);
-
- if (!entry) {
- throw new Error(
- `No registered entry found for identifier: ${identifier}`
- );
- }
+ public register = (key: UniqueIdentifier, value: T) => {
+ const current = this.map.peek();
- return entry;
- });
- }
+ if (current.get(key) === value) {
+ return;
+ }
- public register = (key: UniqueIdentifier, value: T) => {
- const updatedMap = new Map(this.map.peek());
+ const updatedMap = new Map(current);
updatedMap.set(key, value);
this.map.value = updatedMap;
@@ -44,11 +34,13 @@ class Registry {
};
public unregister = (key: UniqueIdentifier, value: T) => {
- if (this.get(key) !== value) {
+ const current = this.map.peek();
+
+ if (current.get(key) !== value) {
return;
}
- const updatedMap = new Map(this.map.peek());
+ const updatedMap = new Map(current);
updatedMap.delete(key);
this.map.value = updatedMap;
diff --git a/packages/abstract/src/plugins/plugin.ts b/packages/abstract/src/plugins/plugin.ts
index 2d540b05..778ee929 100644
--- a/packages/abstract/src/plugins/plugin.ts
+++ b/packages/abstract/src/plugins/plugin.ts
@@ -1,3 +1,4 @@
+import {reactive, untracked} from '@dnd-kit/state';
import type {DragDropManager} from '../manager';
import type {PluginOptions} from './types';
@@ -17,11 +18,14 @@ export class Plugin<
/**
* Whether the plugin instance is disabled.
+ * Triggers effects when accessed.
*/
+ @reactive
public disabled: boolean = false;
/**
* Enable a disabled plugin instance.
+ * Triggers effects.
*/
public enable() {
this.disabled = false;
@@ -29,11 +33,22 @@ export class Plugin<
/**
* Disable an enabled plugin instance.
+ * Triggers effects.
*/
public disable() {
this.disabled = true;
}
+ /**
+ * Whether the plugin instance is disabled.
+ * Does not trigger effects when accessed.
+ */
+ public isDisabled() {
+ return untracked(() => {
+ return this.disabled;
+ });
+ }
+
/**
* Configure a plugin instance with new options.
*/
diff --git a/packages/abstract/src/plugins/registry.ts b/packages/abstract/src/plugins/registry.ts
index adf999af..1f8fc7ff 100644
--- a/packages/abstract/src/plugins/registry.ts
+++ b/packages/abstract/src/plugins/registry.ts
@@ -26,6 +26,12 @@ export class PluginRegistry<
plugin: X,
options?: InferPluginOptions
): InstanceType {
+ const existingInstance = this.instances.get(plugin);
+
+ if (existingInstance) {
+ return existingInstance as InstanceType;
+ }
+
const instance = new plugin(this.manager, options) as U;
this.instances.set(plugin, instance);
diff --git a/packages/collision/src/algorithms/closestCenter.ts b/packages/collision/src/algorithms/closestCenter.ts
index d478ba61..2f9ae9c9 100644
--- a/packages/collision/src/algorithms/closestCenter.ts
+++ b/packages/collision/src/algorithms/closestCenter.ts
@@ -5,12 +5,9 @@ import {Point} from '@dnd-kit/geometry';
import {defaultCollisionDetection} from './default';
/**
- * Returns the closest droppable shape to the pointer coordinates.
- * In the absence of pointer coordinates, return the closest shape to the
- * collision shape.
+ * Returns the distance between the droppable shape and the drag operation coordinates.
*/
export const closestCenter: CollisionDetector = (input) => {
- // TODO: Should dragOperation expose pointer coordinates?
const {dragOperation, droppable} = input;
const {shape, position} = dragOperation;
diff --git a/packages/collision/src/index.ts b/packages/collision/src/index.ts
index 8573f7f2..687118d0 100644
--- a/packages/collision/src/index.ts
+++ b/packages/collision/src/index.ts
@@ -1,2 +1,7 @@
export type {CollisionDetector} from '@dnd-kit/abstract';
-export * from './algorithms';
+export {
+ closestCenter,
+ defaultCollisionDetection,
+ pointerIntersection,
+ shapeIntersection,
+} from './algorithms';
diff --git a/packages/dom-utilities/src/index.ts b/packages/dom-utilities/src/index.ts
index ca468d63..f269c34c 100644
--- a/packages/dom-utilities/src/index.ts
+++ b/packages/dom-utilities/src/index.ts
@@ -15,12 +15,13 @@ export {
getScrollableAncestors,
isDocumentScrollingElement,
ScrollDirection,
+ scrollIntoViewIfNeeded,
} from './scroll';
export {scheduler, Scheduler} from './scheduler';
export {InlineStyles} from './styles';
-export {supportsViewTransition} from './type-guards';
+export {supportsViewTransition, isKeyboardEvent} from './type-guards';
export {inverseTransform} from './transform';
diff --git a/packages/dom-utilities/src/scheduler/scheduler.ts b/packages/dom-utilities/src/scheduler/scheduler.ts
index bbe66b58..a0ec79cf 100644
--- a/packages/dom-utilities/src/scheduler/scheduler.ts
+++ b/packages/dom-utilities/src/scheduler/scheduler.ts
@@ -2,8 +2,12 @@ export class Scheduler {
private animationFrame: number | undefined;
private tasks: (() => void)[] = [];
- public schedule(task: () => void) {
- this.tasks.push(task);
+ public schedule(task: () => void, unshift = false) {
+ if (unshift) {
+ this.tasks.unshift(task);
+ } else {
+ this.tasks.push(task);
+ }
if (!this.animationFrame) {
this.animationFrame = requestAnimationFrame(this.flush);
diff --git a/packages/dom-utilities/src/scroll/index.ts b/packages/dom-utilities/src/scroll/index.ts
index 919fe76b..54825ec9 100644
--- a/packages/dom-utilities/src/scroll/index.ts
+++ b/packages/dom-utilities/src/scroll/index.ts
@@ -15,3 +15,4 @@ export {getScrollPosition} from './getScrollPosition';
export {isDocumentScrollingElement} from './documentScrollingElement';
export {isScrollable} from './isScrollable';
export {isFixed} from './isFixed';
+export {scrollIntoViewIfNeeded} from './scrollIntoViewIfNeeded';
diff --git a/packages/dom-utilities/src/scroll/scrollIntoViewIfNeeded.ts b/packages/dom-utilities/src/scroll/scrollIntoViewIfNeeded.ts
new file mode 100644
index 00000000..fbbb7894
--- /dev/null
+++ b/packages/dom-utilities/src/scroll/scrollIntoViewIfNeeded.ts
@@ -0,0 +1,71 @@
+import {getScrollableAncestors} from './getScrollableAncestors';
+
+function supportsScrollIntoViewIfNeeded(
+ element: Element
+): element is Element & {
+ scrollIntoViewIfNeeded: (centerIfNeeded?: boolean) => void;
+} {
+ return (
+ 'scrollIntoViewIfNeeded' in element &&
+ typeof element.scrollIntoViewIfNeeded === 'function'
+ );
+}
+
+export function scrollIntoViewIfNeeded(el: Element, centerIfNeeded = false) {
+ if (supportsScrollIntoViewIfNeeded(el)) {
+ el.scrollIntoViewIfNeeded(centerIfNeeded);
+ return;
+ }
+
+ if (!(el instanceof HTMLElement)) {
+ return el.scrollIntoView();
+ }
+
+ var [parent] = getScrollableAncestors(el, {limit: 1});
+
+ if (!(parent instanceof HTMLElement)) {
+ return;
+ }
+
+ const parentComputedStyle = window.getComputedStyle(parent, null),
+ parentBorderTopWidth = parseInt(
+ parentComputedStyle.getPropertyValue('border-top-width')
+ ),
+ parentBorderLeftWidth = parseInt(
+ parentComputedStyle.getPropertyValue('border-left-width')
+ ),
+ overTop = el.offsetTop - parent.offsetTop < parent.scrollTop,
+ overBottom =
+ el.offsetTop - parent.offsetTop + el.clientHeight - parentBorderTopWidth >
+ parent.scrollTop + parent.clientHeight,
+ overLeft = el.offsetLeft - parent.offsetLeft < parent.scrollLeft,
+ overRight =
+ el.offsetLeft -
+ parent.offsetLeft +
+ el.clientWidth -
+ parentBorderLeftWidth >
+ parent.scrollLeft + parent.clientWidth,
+ alignWithTop = overTop && !overBottom;
+
+ if ((overTop || overBottom) && centerIfNeeded) {
+ parent.scrollTop =
+ el.offsetTop -
+ parent.offsetTop -
+ parent.clientHeight / 2 -
+ parentBorderTopWidth +
+ el.clientHeight / 2;
+ }
+
+ if ((overLeft || overRight) && centerIfNeeded) {
+ parent.scrollLeft =
+ el.offsetLeft -
+ parent.offsetLeft -
+ parent.clientWidth / 2 -
+ parentBorderLeftWidth +
+ el.clientWidth / 2;
+ }
+
+ if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
+ el.scrollIntoView(alignWithTop);
+ }
+}
diff --git a/packages/dom-utilities/src/type-guards/index.ts b/packages/dom-utilities/src/type-guards/index.ts
index bff4ee4f..eb0672c0 100644
--- a/packages/dom-utilities/src/type-guards/index.ts
+++ b/packages/dom-utilities/src/type-guards/index.ts
@@ -1,5 +1,6 @@
export {isDocument} from './isDocument';
export {isHTMLElement} from './isHTMLElement';
+export {isKeyboardEvent} from './isKeyboardEvent';
export {isNode} from './isNode';
export {isSVGElement} from './isSVGElement';
export {isWindow} from './isWindow';
diff --git a/packages/dom-utilities/src/type-guards/isKeyboardEvent.ts b/packages/dom-utilities/src/type-guards/isKeyboardEvent.ts
new file mode 100644
index 00000000..75927505
--- /dev/null
+++ b/packages/dom-utilities/src/type-guards/isKeyboardEvent.ts
@@ -0,0 +1,5 @@
+export function isKeyboardEvent(
+ event: Event | null | undefined
+): event is KeyboardEvent {
+ return event instanceof KeyboardEvent;
+}
diff --git a/packages/dom/src/modifiers/DragSourceDeltaModifier.ts b/packages/dom/src/modifiers/DragSourceDeltaModifier.ts
index a22c846c..c299bd12 100644
--- a/packages/dom/src/modifiers/DragSourceDeltaModifier.ts
+++ b/packages/dom/src/modifiers/DragSourceDeltaModifier.ts
@@ -5,6 +5,7 @@ import {derived, effect, reactive} from '@dnd-kit/state';
import {DragDropManager} from '../manager';
import {DOMRectangle} from '../shapes';
+// TODO: Need to account for scroll delta
export class DragSourceDeltaModifier extends Modifier {
constructor(manager: DragDropManager) {
super(manager);
@@ -56,6 +57,8 @@ export class DragSourceDeltaModifier extends Modifier {
return transform;
}
+ // console.log(boundingRectangleDelta);
+
return {
x: transform.x - boundingRectangleDelta.left,
y: transform.y - boundingRectangleDelta.top,
diff --git a/packages/dom/src/plugins/feedback/CloneFeedback.ts b/packages/dom/src/plugins/feedback/CloneFeedback.ts
index c27c515d..8b70eae1 100644
--- a/packages/dom/src/plugins/feedback/CloneFeedback.ts
+++ b/packages/dom/src/plugins/feedback/CloneFeedback.ts
@@ -1,6 +1,6 @@
import {Plugin} from '@dnd-kit/abstract';
import type {CleanupFunction} from '@dnd-kit/types';
-import {effect} from '@dnd-kit/state';
+import {effect, untracked} from '@dnd-kit/state';
import {cloneElement} from '@dnd-kit/dom-utilities';
import type {DragDropManager} from '../../manager';
@@ -21,16 +21,16 @@ export class CloneFeedback extends Plugin {
const {status, source} = dragOperation;
const isDragging = status === 'dragging';
- if (
- !isDragging ||
- !source ||
- !source.element ||
- source.feedback !== CloneFeedback
- ) {
+ if (!isDragging || !source || source.feedback !== CloneFeedback) {
+ return;
+ }
+
+ const element = untracked(() => source.element);
+
+ if (!element) {
return;
}
- const {element} = source;
const {boundingRectangle} = new DOMRectangle(element);
const overlay = createOverlay(manager, boundingRectangle);
diff --git a/packages/dom/src/plugins/feedback/Overlay.ts b/packages/dom/src/plugins/feedback/Overlay.ts
index f82f31a4..9c573ee0 100644
--- a/packages/dom/src/plugins/feedback/Overlay.ts
+++ b/packages/dom/src/plugins/feedback/Overlay.ts
@@ -1,6 +1,7 @@
import {effect} from '@dnd-kit/state';
import {
InlineStyles,
+ isKeyboardEvent,
supportsViewTransition,
scheduler,
} from '@dnd-kit/dom-utilities';
@@ -33,6 +34,9 @@ class Overlay {
const style = document.createElement('style');
element.style.setProperty('all', 'initial');
+ element.style.setProperty('display', 'flex');
+ element.style.setProperty('align-items', 'stretch');
+ element.style.setProperty('justify-content', 'stretch');
element.style.setProperty('pointer-events', 'none');
element.style.setProperty('position', 'fixed');
element.style.setProperty('top', `${top}px`);
@@ -40,6 +44,11 @@ class Overlay {
element.style.setProperty('width', `${width}px`);
element.style.setProperty('height', `${height}px`);
element.style.setProperty('touch-action', 'none');
+ element.style.setProperty('z-index', '9999');
+
+ if (isKeyboardEvent(manager.dragOperation.activatorEvent)) {
+ element.style.setProperty('transition', 'transform 150ms ease');
+ }
element.setAttribute('data-dnd-kit-overlay', '');
style.innerText = `dialog[data-dnd-kit-overlay]::backdrop {display: none;}`;
@@ -47,6 +56,29 @@ class Overlay {
this.element = element;
+ effect(() => {
+ const {source} = manager.dragOperation;
+
+ if (!source || !source.element) {
+ return;
+ }
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const {target} = entry;
+ const {width, height} = target.getBoundingClientRect();
+
+ element.style.setProperty('width', `${width}px`);
+ element.style.setProperty('height', `${height}px`);
+ }
+ });
+ resizeObserver.observe(source.element);
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ });
+
const effectCleanup = effect(() => {
const {dragOperation} = manager;
const {initialized, transform} = dragOperation;
@@ -57,12 +89,11 @@ class Overlay {
scheduler.schedule(() => {
const {x, y} = this.transform;
+ dragOperation.shape = new DOMRectangle(element, true).translate(x, y);
element.style.setProperty(
'transform',
`translate3d(${x}px, ${y}px, 0)`
);
-
- dragOperation.shape = new DOMRectangle(element);
});
}
});
diff --git a/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts
index 8f2517aa..497875a1 100644
--- a/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts
+++ b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts
@@ -113,8 +113,6 @@ export class PlaceholderFeedback extends Plugin {
if (Array.from(mutation.addedNodes).includes(element)) {
ignoreNextMutation = true;
- console.log(mutation);
-
element.replaceWith(placeholder);
overlay.appendChild(element);
diff --git a/packages/dom/src/plugins/scrolling/Scroller.ts b/packages/dom/src/plugins/scrolling/Scroller.ts
index 831c1b18..83ad31e0 100644
--- a/packages/dom/src/plugins/scrolling/Scroller.ts
+++ b/packages/dom/src/plugins/scrolling/Scroller.ts
@@ -6,6 +6,7 @@ import {
getScrollableAncestors,
ScrollDirection,
scheduler,
+ isKeyboardEvent,
} from '@dnd-kit/dom-utilities';
import {Axes, type Coordinates} from '@dnd-kit/geometry';
import {isEqual} from '@dnd-kit/utilities';
@@ -56,6 +57,22 @@ export class Scroller extends Plugin {
};
this.scrollIntentTracker = new ScrollIntentTracker(manager);
+
+ this.destroy = manager.monitor.addEventListener('dragmove', (event) => {
+ if (
+ this.disabled ||
+ event.defaultPrevented ||
+ !isKeyboardEvent(manager.dragOperation.activatorEvent) ||
+ !event.by
+ ) {
+ return;
+ }
+
+ // Prevent the move event if we can scroll to the new coordinates
+ if (this.scroll({by: event.by})) {
+ event.preventDefault();
+ }
+ });
}
public scroll = (options?: {by: Coordinates}): boolean => {
diff --git a/packages/dom/src/plugins/sortable/SortableKeyboardPlugin.ts b/packages/dom/src/plugins/sortable/SortableKeyboardPlugin.ts
new file mode 100644
index 00000000..39137610
--- /dev/null
+++ b/packages/dom/src/plugins/sortable/SortableKeyboardPlugin.ts
@@ -0,0 +1,178 @@
+import {batch, effect} from '@dnd-kit/state';
+import {Plugin} from '@dnd-kit/abstract';
+import {closestCenter} from '@dnd-kit/collision';
+import {
+ scheduler,
+ isKeyboardEvent,
+ scrollIntoViewIfNeeded,
+} from '@dnd-kit/dom-utilities';
+import type {Coordinates} from '@dnd-kit/geometry';
+
+import type {Droppable} from '../../nodes';
+import {DragDropManager} from '../../manager';
+import {DOMRectangle} from '../../shapes';
+
+import {isSortable} from './registry';
+import {Scroller} from '../scrolling';
+
+export class SortableKeyboardPlugin extends Plugin {
+ constructor(manager: DragDropManager) {
+ super(manager);
+
+ const effectCleanup = effect(() => {
+ const {dragOperation} = manager;
+
+ if (!isKeyboardEvent(dragOperation.activatorEvent)) {
+ return;
+ }
+
+ if (!isSortable(dragOperation.source)) {
+ return;
+ }
+
+ if (dragOperation.initialized) {
+ const scroller = manager.plugins.get(Scroller);
+
+ if (scroller) {
+ scroller.disable();
+
+ return () => scroller.enable();
+ }
+ }
+ });
+
+ const unsubscribe = manager.monitor.addEventListener(
+ 'dragmove',
+ (event, manager) => {
+ if (this.disabled) {
+ return;
+ }
+
+ const {dragOperation} = manager;
+
+ if (!isKeyboardEvent(dragOperation.activatorEvent)) {
+ return;
+ }
+
+ if (!isSortable(dragOperation.source)) {
+ return;
+ }
+
+ if (!dragOperation.shape) {
+ return;
+ }
+
+ const {actions, collisionObserver, registry} = manager;
+ const {by} = event;
+
+ if (!by) {
+ return;
+ }
+
+ const direction = getDirection(by);
+ const {boundingRectangle} = dragOperation.shape;
+ const potentialTargets: Droppable[] = [];
+
+ for (const droppable of registry.droppable) {
+ const {shape, id} = droppable;
+
+ if (
+ !shape ||
+ (id === dragOperation.source?.id && isSortable(droppable))
+ ) {
+ continue;
+ }
+
+ switch (direction) {
+ case 'down':
+ if (boundingRectangle.top < shape.boundingRectangle.top) {
+ potentialTargets.push(droppable);
+ }
+ break;
+ case 'up':
+ if (boundingRectangle.top > shape.boundingRectangle.top) {
+ potentialTargets.push(droppable);
+ }
+ break;
+ case 'left':
+ if (boundingRectangle.left > shape.boundingRectangle.left) {
+ potentialTargets.push(droppable);
+ }
+ break;
+ case 'right':
+ if (boundingRectangle.left < shape.boundingRectangle.left) {
+ potentialTargets.push(droppable);
+ }
+ break;
+ }
+ }
+
+ event.preventDefault();
+ collisionObserver.disable();
+
+ const collisions = collisionObserver.computeCollisions(
+ potentialTargets,
+ closestCenter
+ );
+ const [firstCollision] = collisions;
+
+ if (!firstCollision) {
+ return;
+ }
+
+ const {id} = firstCollision;
+
+ actions.setDropTarget(id);
+
+ scheduler.schedule(() => {
+ const {shape, source} = dragOperation;
+
+ if (!shape || !source?.element) {
+ return;
+ }
+
+ scrollIntoViewIfNeeded(source.element);
+
+ scheduler.schedule(() => {
+ if (!source.element) {
+ return;
+ }
+
+ const {center} = new DOMRectangle(source.element, true);
+
+ batch(() => {
+ actions.setDropTarget(source.id);
+ actions.move({
+ to: {
+ x: center.x,
+ y: center.y,
+ },
+ });
+ });
+
+ collisionObserver.enable();
+ });
+ }, true);
+ }
+ );
+
+ this.destroy = () => {
+ unsubscribe();
+ effectCleanup();
+ };
+ }
+}
+
+function getDirection(delta: Coordinates) {
+ const {x, y} = delta;
+
+ if (x > 0) {
+ return 'right';
+ } else if (x < 0) {
+ return 'left';
+ } else if (y > 0) {
+ return 'down';
+ } else if (y < 0) {
+ return 'up';
+ }
+}
diff --git a/packages/dom/src/plugins/sortable/registry.ts b/packages/dom/src/plugins/sortable/registry.ts
new file mode 100644
index 00000000..ce80558c
--- /dev/null
+++ b/packages/dom/src/plugins/sortable/registry.ts
@@ -0,0 +1,7 @@
+import type {Droppable, Draggable} from '../../nodes';
+
+export const SortableRegistry = new WeakSet();
+
+export function isSortable(element: Draggable | Droppable | null): boolean {
+ return element ? SortableRegistry.has(element) : false;
+}
diff --git a/packages/dom/src/plugins/sortable/sortable.ts b/packages/dom/src/plugins/sortable/sortable.ts
index d5262d29..4d2e75d0 100644
--- a/packages/dom/src/plugins/sortable/sortable.ts
+++ b/packages/dom/src/plugins/sortable/sortable.ts
@@ -2,8 +2,9 @@ import type {
Data,
DragDropManager as AbstractDragDropManager,
} from '@dnd-kit/abstract';
-import {effect, reactive, untracked} from '@dnd-kit/state';
+import {batch, effect, reactive, untracked} from '@dnd-kit/state';
import type {Type, UniqueIdentifier} from '@dnd-kit/types';
+import {scheduler} from '@dnd-kit/dom-utilities';
import {Draggable, Droppable} from '../../nodes';
import type {
@@ -12,7 +13,9 @@ import type {
DroppableInput,
} from '../../nodes';
import type {Sensors} from '../../sensors';
-import {DOMRectangle} from '../../shapes';
+
+import {SortableKeyboardPlugin} from './SortableKeyboardPlugin';
+import {SortableRegistry} from './registry';
export interface SortableInput
extends DraggableInput,
@@ -28,11 +31,19 @@ export class Sortable {
index: number;
constructor(
- {index, ...input}: SortableInput,
+ {index, sensors, ...input}: SortableInput,
protected manager: AbstractDragDropManager
) {
- this.draggable = new Draggable(input, manager);
- this.droppable = new Droppable({...input, ignoreTransform: true}, manager);
+ this.draggable = new Draggable({...input, sensors}, manager);
+ this.droppable = new Droppable(
+ {...input, ignoreTransform: true},
+ manager
+ );
+
+ SortableRegistry.add(this.draggable);
+ SortableRegistry.add(this.droppable);
+
+ manager.plugins.register(SortableKeyboardPlugin);
let previousIndex = index;
@@ -60,15 +71,17 @@ export class Sortable {
};
if (delta.x || delta.y) {
- element.animate(
- {
- transform: [
- `translate3d(${delta.x}px, ${delta.y}px, 0)`,
- 'translate3d(0, 0, 0)',
- ],
- },
- {duration: 150, easing: 'ease'}
- );
+ scheduler.schedule(() => {
+ element.animate(
+ {
+ transform: [
+ `translate3d(${delta.x}px, ${delta.y}px, 0)`,
+ 'translate3d(0, 0, 0)',
+ ],
+ },
+ {duration: 150, easing: 'ease'}
+ );
+ });
}
};
@@ -100,8 +113,17 @@ export class Sortable {
}
public set disabled(value: boolean) {
- this.draggable.disabled = value;
- this.droppable.disabled = value;
+ batch(() => {
+ this.draggable.disabled = value;
+ this.droppable.disabled = value;
+ });
+ }
+
+ public set data(data: T | null) {
+ batch(() => {
+ this.draggable.data = data;
+ this.droppable.data = data;
+ });
}
public set activator(activator: Element | undefined) {
@@ -109,13 +131,17 @@ export class Sortable {
}
public set element(element: Element | undefined) {
- this.draggable.element = element;
- this.droppable.element = element;
+ batch(() => {
+ this.draggable.element = element;
+ this.droppable.element = element;
+ });
}
public set id(id: UniqueIdentifier) {
- this.draggable.id = id;
- this.droppable.id = id;
+ batch(() => {
+ this.draggable.id = id;
+ this.droppable.id = id;
+ });
}
public set sensors(value: Sensors | undefined) {
@@ -147,5 +173,3 @@ export class Sortable {
this.droppable.destroy();
}
}
-
-function noop() {}
diff --git a/packages/dom/src/sensors/drag/DragSensor.ts b/packages/dom/src/sensors/drag/DragSensor.ts
index 7301f8d3..6dfa66b1 100644
--- a/packages/dom/src/sensors/drag/DragSensor.ts
+++ b/packages/dom/src/sensors/drag/DragSensor.ts
@@ -142,6 +142,7 @@ export class DragSensor extends Sensor {
if (this.manager.dragOperation.status === 'idle') {
this.manager.actions.start({
+ event,
coordinates: {
x: event.clientX,
y: event.clientY,
@@ -151,7 +152,7 @@ export class DragSensor extends Sensor {
}
this.manager.actions.move({
- coordinates: {
+ to: {
x: event.clientX,
y: event.clientY,
},
diff --git a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts
index 3b817330..ae84e9f8 100644
--- a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts
+++ b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts
@@ -7,6 +7,7 @@ import type {DragDropManager} from '../../manager';
import type {Draggable} from '../../nodes';
import {AutoScroller, Scroller} from '../../plugins';
import {DOMRectangle} from '../../shapes';
+import {Coordinates} from '@dnd-kit/geometry';
export type KeyCode = KeyboardEvent['code'];
@@ -108,6 +109,7 @@ export class KeyboardSensor extends Sensor<
this.manager.actions.setDragSource(source.id);
this.manager.actions.start({
+ event,
coordinates: {
x: center.x,
y: center.y,
@@ -131,44 +133,16 @@ export class KeyboardSensor extends Sensor<
return;
}
- const {shape} = this.manager.dragOperation;
-
- if (!shape) {
- return;
- }
-
- const {center} = shape;
- const factor = event.shiftKey ? 5 : 1;
- const offset = {
- x: 0,
- y: 0,
- };
-
if (isKeycode(event, keyboardCodes.up)) {
- offset.y = -DEFAULT_OFFSET * factor;
+ this.handleMove('up', event);
} else if (isKeycode(event, keyboardCodes.down)) {
- offset.y = DEFAULT_OFFSET * factor;
+ this.handleMove('down', event);
}
if (isKeycode(event, keyboardCodes.left)) {
- offset.x = -DEFAULT_OFFSET * factor;
+ this.handleMove('left', event);
} else if (isKeycode(event, keyboardCodes.right)) {
- offset.x = DEFAULT_OFFSET * factor;
- }
-
- if (offset.x || offset.y) {
- event.preventDefault();
-
- const scroller = this.manager.plugins.get(Scroller);
-
- if (!scroller?.scroll({by: offset})) {
- this.manager.actions.move({
- coordinates: {
- x: center.x + offset.x,
- y: center.y + offset.y,
- },
- });
- }
+ this.handleMove('right', event);
}
};
@@ -177,6 +151,45 @@ export class KeyboardSensor extends Sensor<
]);
};
+ protected handleMove(
+ direction: 'up' | 'down' | 'left' | 'right',
+ event: KeyboardEvent
+ ) {
+ const {shape} = this.manager.dragOperation;
+ const factor = event.shiftKey ? 5 : 1;
+ let offset = {
+ x: 0,
+ y: 0,
+ };
+
+ if (!shape) {
+ return;
+ }
+
+ switch (direction) {
+ case 'up':
+ offset = {x: 0, y: -DEFAULT_OFFSET * factor};
+ break;
+ case 'down':
+ offset = {x: 0, y: DEFAULT_OFFSET * factor};
+ break;
+ case 'left':
+ offset = {x: -DEFAULT_OFFSET * factor, y: 0};
+ break;
+ case 'right':
+ offset = {x: DEFAULT_OFFSET * factor, y: 0};
+ break;
+ }
+
+ if (offset?.x || offset?.y) {
+ event.preventDefault();
+
+ this.manager.actions.move({
+ by: offset,
+ });
+ }
+ }
+
private sideEffects(): CleanupFunction {
const effectCleanupFns: CleanupFunction[] = [];
diff --git a/packages/dom/src/sensors/pointer/PointerSensor.ts b/packages/dom/src/sensors/pointer/PointerSensor.ts
index 109b474f..124244c3 100644
--- a/packages/dom/src/sensors/pointer/PointerSensor.ts
+++ b/packages/dom/src/sensors/pointer/PointerSensor.ts
@@ -115,13 +115,16 @@ export class PointerSensor extends Sensor<
const {activationConstraints} = options;
if (!activationConstraints?.delay && !activationConstraints?.distance) {
- this.handleStart(source);
+ this.handleStart(source, event);
event.stopImmediatePropagation();
} else {
const {delay} = activationConstraints;
if (delay) {
- const timeout = setTimeout(() => this.handleStart(source), delay.value);
+ const timeout = setTimeout(
+ () => this.handleStart(source, event),
+ delay.value
+ );
this.clearTimeout = () => {
clearTimeout(timeout);
@@ -183,7 +186,7 @@ export class PointerSensor extends Sensor<
event.preventDefault();
event.stopPropagation();
- this.manager.actions.move({coordinates});
+ this.manager.actions.move({to: coordinates});
return;
}
@@ -206,7 +209,7 @@ export class PointerSensor extends Sensor<
return this.handleCancel();
}
if (exceedsDistance(delta, distance.value)) {
- return this.handleStart(source);
+ return this.handleStart(source, event);
}
}
@@ -236,7 +239,7 @@ export class PointerSensor extends Sensor<
}
}
- protected handleStart(source: Draggable) {
+ protected handleStart(source: Draggable, event: PointerEvent) {
this.clearTimeout?.();
if (
@@ -247,7 +250,7 @@ export class PointerSensor extends Sensor<
}
this.manager.actions.setDragSource(source.id);
- this.manager.actions.start({coordinates: this.initialCoordinates});
+ this.manager.actions.start({coordinates: this.initialCoordinates, event});
}
protected handleCancel() {
diff --git a/packages/react/src/draggable/useDraggable.ts b/packages/react/src/draggable/useDraggable.ts
index 38bfcee2..29c25485 100644
--- a/packages/react/src/draggable/useDraggable.ts
+++ b/packages/react/src/draggable/useDraggable.ts
@@ -48,6 +48,12 @@ export function useDraggable(
return {
isDragSource,
+ activatorRef: useCallback(
+ (element: Element | null) => {
+ draggable.activator = element ?? undefined;
+ },
+ [draggable]
+ ),
ref: useCallback(
(element: Element | null) => {
draggable.element = element ?? undefined;
diff --git a/packages/react/src/sortable/useSortable.ts b/packages/react/src/sortable/useSortable.ts
index eb505854..dc0167a3 100644
--- a/packages/react/src/sortable/useSortable.ts
+++ b/packages/react/src/sortable/useSortable.ts
@@ -19,7 +19,7 @@ export interface UseSortableInput
}
export function useSortable(input: UseSortableInput) {
- const {id, index, disabled, feedback = CloneFeedback, sensors} = input;
+ const {id, data, index, disabled, feedback = CloneFeedback, sensors} = input;
const activator = getCurrentValue(input.activator);
const element = getCurrentValue(input.element);
const manager = useDragDropManager();
@@ -32,6 +32,7 @@ export function useSortable(input: UseSortableInput) {
const isDragSource = useComputed(() => sortable.isDragSource).value;
useOnValueChange(id, () => (sortable.id = id));
+ useOnValueChange(data, () => (sortable.data = data ?? null));
useOnValueChange(index, () => (sortable.index = index));
useOnValueChange(activator, () => (sortable.activator = activator));
useOnValueChange(element, () => (sortable.element = element));
@@ -56,6 +57,12 @@ export function useSortable(input: UseSortableInput) {
get isDropTarget() {
return isDropTarget.value;
},
+ activatorRef: useCallback(
+ (element: Element | null) => {
+ sortable.activator = element ?? undefined;
+ },
+ [sortable]
+ ),
ref: useCallback(
(element: Element | null) => {
sortable.element = element ?? undefined;