This release introduces significant API changes to improve type safety, flexibility, and performance. The main focus is on refactoring the sensor type system from array-based to union-based generics, enabling dynamic sensor updates, and improving React hook performance.
Changed: The generic type parameter S in Draggable and related types has changed from Sensor[] to Sensor (union type).
Before:
class Draggable<S extends Sensor[] = Sensor[]>After:
class Draggable<S extends Sensor = Sensor>Impact:
- Type inference now works with union types instead of arrays
- Constructor still accepts an array:
new Draggable([sensor1, sensor2]) - All related types (
DraggableDrag,DraggableDragItem,DraggableModifier, etc.) have been updated accordingly
Migration:
// Before
const draggable = new Draggable<[PointerSensor, KeyboardSensor]>([pointerSensor, keyboardSensor]);
// After
const draggable = new Draggable<PointerSensor | KeyboardSensor>([pointerSensor, keyboardSensor]);Changed: The sensors property is now writable and can be updated after instantiation.
Before:
readonly sensors: S; // Array, readonlyAfter:
get sensors(): ReadonlyArray<S>;
set sensors(sensors: readonly S[]);Impact:
- You can now dynamically add/remove sensors without recreating the draggable instance
- Setting sensors automatically unbinds removed sensors and binds new ones
- If an active drag's sensor is removed, the drag is automatically stopped
Migration:
// Before - had to recreate draggable
const draggable = new Draggable([sensor1]);
// ... later
draggable.destroy();
const newDraggable = new Draggable([sensor1, sensor2]);
// After - can update sensors directly
const draggable = new Draggable([sensor1]);
// ... later
draggable.sensors = [sensor1, sensor2];Changed: Default value changed from new Set() to undefined.
Before:
dndGroups: new Set();After:
dndGroups: undefined;Impact:
- If you relied on the default being an empty Set, you'll need to explicitly provide one
Migration:
// If you need an empty Set, provide it explicitly
new Draggable(sensors, { dndGroups: new Set() });Changed: The element parameter in Droppable constructor can now be null.
Before:
constructor(element: HTMLElement | SVGSVGElement, options?: DroppableOptions)After:
constructor(element: HTMLElement | SVGSVGElement | null, options?: DroppableOptions)Impact:
- Allows creating droppables without an element initially
- The
elementproperty is nowHTMLElement | SVGSVGElement | null
Changed: Method no longer accepts a rect parameter.
Before:
updateClientRect(rect?: Rect): voidAfter:
updateClientRect(): voidImpact:
- The method now uses
computeClientRectcallback instead of accepting a parameter - If you were passing a rect manually, you need to use
computeClientRectoption instead
Migration:
// Before
droppable.updateClientRect({ x: 0, y: 0, width: 100, height: 100 });
// After
const droppable = new Droppable(element, {
computeClientRect: () => ({ x: 0, y: 0, width: 100, height: 100 }),
});
droppable.updateClientRect();Changed: Removed automatic filtering that prevented draggables from colliding with droppables they contain.
Before:
- Draggables were automatically excluded from colliding with droppables if the draggable's element contained the droppable's element
After:
- No automatic filtering - all collisions are detected based on
acceptpredicate only
Impact:
- If you relied on this implicit behavior, you'll need to add explicit filtering in your
acceptpredicate
Migration:
// Before - implicit filtering
// Draggable would never collide with droppable if it contained it
// After - explicit filtering needed
const droppable = new Droppable(element, {
accept: (draggable) => {
// Explicitly check if draggable contains this droppable
const items = draggable.drag?.items || [];
return !items.some((item) => item.element.contains(element));
},
});Changed: The SensorsEventsType<S extends Sensor[]> type has been removed.
Before:
import type { SensorsEventsType } from 'dragdoll/sensors/sensor';
type Events = SensorsEventsType<[PointerSensor, KeyboardSensor]>;After:
- Use
S['_events_type']directly whereS extends Sensor
Migration:
// Before
type Events = SensorsEventsType<[PointerSensor, KeyboardSensor]>;
// After
type Events = (PointerSensor | KeyboardSensor)['_events_type'];Changed: All React hooks now use Sensor union type instead of Sensor[].
Before:
useDraggable<S extends Sensor[] = Sensor[]>(sensors: (S[number] | null)[])After:
useDraggable<S extends Sensor = Sensor>(sensors: (S | null)[])Impact:
- Type inference works with union types
- Same migration pattern as core library
Migration:
// Before
const draggable = useDraggable<[PointerSensor, KeyboardSensor]>([pointerSensor, keyboardSensor]);
// After
const draggable = useDraggable<PointerSensor | KeyboardSensor>([pointerSensor, keyboardSensor]);Changed: The hook no longer recreates the draggable instance when sensors change.
Before:
- Changing sensors array would destroy and recreate the draggable
After:
- Changing sensors array updates the existing draggable's sensors property
- Draggable is only recreated when
idchanges
Impact:
- Better performance - no unnecessary instance recreation
- State is preserved when sensors change
- If you relied on recreation behavior, you may need to adjust your code
Changed: The Start event callback now receives the drag object directly.
Before:
useDraggableCallback(draggable, DraggableEventType.Start, () => {
setDrag(draggable?.drag || null);
});After:
useDraggableCallback(draggable, DraggableEventType.Start, (drag) => {
setDrag(drag);
});Impact:
- The callback signature matches the event payload structure
- More consistent with other event callbacks
Changed:
elementparameter can now benull- Added
computeClientRectoption support - Improved observer management
Before:
useDroppable({ element: HTMLElement });After:
useDroppable({
element: HTMLElement | SVGSVGElement | null,
computeClientRect?: (droppable: Droppable) => Rect
})Impact:
- More flexible element handling
- Can customize client rect calculation
Changed: Settings parameters in sensor hooks are now optional instead of required with default empty object.
Before:
usePointerSensor({}, element);
useKeyboardSensor({}, element);
useKeyboardMotionSensor({}, element);After:
usePointerSensor(undefined, element);
// or
usePointerSensor({ listenerOptions: {} }, element);
useKeyboardSensor(undefined, element);
useKeyboardMotionSensor(undefined, element);Impact:
- More consistent API - settings are truly optional
- TypeScript will catch cases where you might have been relying on default empty object behavior
Migration:
// Before
const sensor = usePointerSensor({}, element);
// After - both are equivalent
const sensor = usePointerSensor(undefined, element);
const sensor = usePointerSensor({}, element);Changed: Default settings exports renamed to follow PascalCase convention.
Before:
import { keyboardSensorDefaults, keyboardMotionSensorDefaults } from 'dragdoll/sensors/keyboard';After:
import {
KeyboardSensorDefaultSettings,
KeyboardMotionSensorDefaultSettings,
PointerSensorDefaultSettings,
DroppableDefaultSettings,
} from 'dragdoll';Impact:
- Breaking change if you imported the old default settings names
- More consistent naming convention
Migration:
// Before
import { keyboardSensorDefaults } from 'dragdoll/sensors/keyboard';
const settings = { ...keyboardSensorDefaults, moveDistance: 50 };
// After
import { KeyboardSensorDefaultSettings } from 'dragdoll/sensors/keyboard';
const settings = { ...KeyboardSensorDefaultSettings, moveDistance: 50 };Sensors can now be updated on Draggable instances without recreating them:
const draggable = new Draggable([pointerSensor]);
// Add a sensor
draggable.sensors = [...draggable.sensors, keyboardSensor];
// Remove a sensor
draggable.sensors = [pointerSensor];All sensor classes now have an updateElement() method to change the tracked element:
// PointerSensor
pointerSensor.updateElement(newElement);
// KeyboardSensor
keyboardSensor.updateElement(newElement);
// KeyboardMotionSensor
keyboardMotionSensor.updateElement(newElement);New option to customize how client rect is calculated:
const droppable = new Droppable(element, {
computeClientRect: (droppable) => {
// Custom rect calculation
return customRect;
},
});Default settings are now exported for easier access (note: renamed from previous versions):
import {
KeyboardSensorDefaultSettings,
KeyboardMotionSensorDefaultSettings,
PointerSensorDefaultSettings,
DroppableDefaultSettings,
} from 'dragdoll';Changed: All sensor updateSettings() methods now check if the sensor is destroyed before updating.
Impact:
- Prevents errors when trying to update settings on destroyed sensors
- More robust error handling
Sensor hooks (usePointerSensor, useKeyboardSensor, useKeyboardMotionSensor) now update the element instead of recreating the sensor:
const sensor = usePointerSensor({}, element);
// When element changes, sensor is updated instead of recreatedSensor hooks now properly merge default settings when updating, ensuring all settings are explicitly provided:
// Hooks now merge defaults when updating settings
usePointerSensor({ listenerOptions: { passive: true } }, element);
// Internally merges with PointerSensorDefaultSettingsNew TypeScript interfaces exported for better type safety:
import {
UsePointerSensorSettings,
UseKeyboardSensorSettings,
UseKeyboardMotionSensorSettings,
} from 'dragdoll-react';- Sensor Management: Improved sensor binding/unbinding logic with better cleanup
- DndObserver: Removed implicit containment filtering for more predictable behavior
- Droppable: Improved
updateClientRect()to usecomputeClientRectcallback - Type Safety: Better type inference with union types instead of array types
- Performance: Reduced unnecessary draggable instance recreation
- State Management: Better handling of sensor and settings updates
- Observer Management: Improved DndObserver add/remove logic in hooks
- Collision Detection: Hooks no longer automatically queue collision checks on every update - you must manually call
detectCollisions()when needed
- Fixed issue where React hooks would recreate draggable instances unnecessarily
- Fixed sensor cleanup when sensors are removed from draggable
- Improved handling of active drags when sensors are removed
- Fixed type inference issues with sensor arrays
- ✅ Update
Draggablegeneric type fromSensor[]toSensorunion - ✅ Update React hook generic types from
Sensor[]toSensorunion - ✅ Replace
SensorsEventsType<S>withS['_events_type'] - ✅ Update
Droppable.updateClientRect()calls if passing parameters - ✅ Add explicit filtering in
acceptpredicate if relying on containment filtering - ✅ Update
dndGroupsdefault if relying on empty Set - ✅ Review React hook behavior - sensors now update instead of recreate
- ✅ Update default settings imports if using old names (
keyboardSensorDefaults→KeyboardSensorDefaultSettings) - ✅ Review sensor hook calls - settings parameter is now optional
Before:
import { Draggable } from 'dragdoll/draggable';
import { PointerSensor, KeyboardSensor } from 'dragdoll/sensors';
import type { SensorsEventsType } from 'dragdoll/sensors/sensor';
type MySensors = [PointerSensor, KeyboardSensor];
type MyEvents = SensorsEventsType<MySensors>;
const pointerSensor = new PointerSensor(element);
const keyboardSensor = new KeyboardSensor(element);
const draggable = new Draggable<MySensors>([pointerSensor, keyboardSensor]);
// Later, to add a sensor:
draggable.destroy();
const newDraggable = new Draggable<MySensors>([pointerSensor, keyboardSensor, newSensor]);After:
import { Draggable } from 'dragdoll/draggable';
import { PointerSensor, KeyboardSensor } from 'dragdoll/sensors';
type MySensors = PointerSensor | KeyboardSensor;
type MyEvents = MySensors['_events_type'];
const pointerSensor = new PointerSensor(element);
const keyboardSensor = new KeyboardSensor(element);
const draggable = new Draggable<MySensors>([pointerSensor, keyboardSensor]);
// Later, to add a sensor:
draggable.sensors = [pointerSensor, keyboardSensor, newSensor];- dragdoll:
0.11.1→0.12.0 - dragdoll-react:
0.1.1→0.2.0
No dependency changes in this release.
Thank you to all contributors who helped with this release!