Drag and Drop
Headless Drag and Drop; covers React Aria useDrag and useDrop, Pragmatic drag and drop adapters, HTML Drag and Drop DataTransfer.
| Package | @vizejs/ui/drag-and-drop |
| Maturity | stable |
| Own the source | vize lib pull drag-and-drop |
| Requires | — |
| Aliases | drag and drop, dnd, drag handle, drop target, drop indicator |
| Covers | React Aria useDrag and useDrop, Pragmatic drag and drop adapters, HTML Drag and Drop DataTransfer |
API
createDragAndDrop
Create an SSR-safe pointer, touch, and keyboard drag-and-drop coordinator.
function createDragAndDrop<Data = unknown>( options: DragAndDropOptions<Data> = {}, ): DragAndDropController<Data>
useDragAndDrop
Create a drag-and-drop coordinator disposed with the current effect scope.
function useDragAndDrop<Data = unknown>( options: DragAndDropOptions<Data> = {}, ): DragAndDropController<Data>
createDragLiveRegion
Create a lazily mounted role="status" live region for drag announcements.
function createDragLiveRegion(): DragLiveRegion
defaultDragAnnouncements
Built-in English announcement builders; consumers override to localize.
const defaultDragAnnouncements: Required<DragAnnouncements>
createDragAutoScroller
Create an edge-proximity auto-scroller for one controller. Each pointer sample applies one immediate scroll step; while a sample stays inside a threshold band, an animation-frame loop keeps scrolling so a held pointer near an edge continues to advance the container.
function createDragAutoScroller( options: DragAutoScrollOptions | undefined, ): DragAutoScroller
readBoolean
Resolve and validate a reactive boolean option for JavaScript consumers.
function readBoolean(value: MaybeBoolean, name: string, defaultValue = false): boolean
readDistance
Resolve a finite non-negative pixel distance with an explicit default.
function readDistance( value: DragAndDropOptions["startDistance"], name: string, defaultValue: number, minimum = 0, ): number
readLabel
Resolve an announcement label falling back to the owning key.
function readLabel(value: DragSourceOptions["label"], key: string): string
validateCallbacks
Validate one callback slot eagerly so setup failures never install listeners.
function validateCallbacks(options: object, names: readonly string[]): void
validateKey
Validate a registration key and its uniqueness inside one controller.
function validateKey( key: unknown, existing: ReadonlySet<string> | ReadonlyMap<string, unknown>, ): string
validateEdges
Validate the closed drop-edge union without accepting mistyped JavaScript.
function validateEdges(edges: DropTargetOptions["edges"]): readonly DropEdge[]
normalizeRect
Normalize a measured rectangle, rejecting rects that cannot own a drop.
function normalizeRect( rect: DropTargetRect | DOMRectReadOnly | null | undefined, ): DropTargetRect | null
measureRect
Measure one element through its override or the live layout.
function measureRect( element: Element | null | undefined, getRect: (() => DropTargetRect | DOMRectReadOnly | null | undefined) | undefined, ): DropTargetRect | null
containsPoint
Whether a client point lies inside an axis-aligned rectangle.
function containsPoint(rect: DropTargetRect, point: Point): boolean
exceedsDistance
Chebyshev distance gate deciding when an armed pointer becomes a session.
function exceedsDistance(origin: Point, point: Point, threshold: number): boolean
resolveEdge
Resolve the drop edge for a point inside a rectangle. When "inside" is allowed together with directional edges, the central half of each constrained axis resolves to "inside"; otherwise the nearest allowed edge on the dominant axis wins.
function resolveEdge( rect: DropTargetRect, point: Point, edges: readonly DropEdge[], ): DropEdge
indicatorFor
Build indicator geometry: a placeholder rect plus a collapsed edge line.
function indicatorFor( targetKey: string, edge: DropEdge, rect: DropTargetRect | null, ): DropIndicatorState
compareDocumentOrder
Sort elements into document order, treating detached elements as later.
function compareDocumentOrder(left: Element, right: Element): number
hitTest
Overlay-safe hit test: measured rectangles are compared directly, so drag previews and overlays can never mask a target. Nested ownership prefers the innermost target by DOM containment, then the smallest area.
function hitTest<Candidate extends HitCandidate>( candidates: readonly Candidate[], point: Point, ): Candidate | null
createDragEvent
Build a frozen lifecycle snapshot for controller-level callbacks.
function createDragEvent<Data, Type extends DragEventType>( type: Type, pointerType: DragPointerType, sourceKey: string, payload: DragPayload<Data> | null, targetKey: string | null, edge: DropEdge | null, point: Point | null, originalEvent: Event | null, isCanceled = false, ): DragLifecycleEvent<Data, Type>
createDropTargetEvent
Build a frozen target-scoped snapshot for drop-target callbacks.
function createDropTargetEvent<Data>( type: DropTargetEventType, targetKey: string, sourceKey: string, pointerType: DragPointerType, payload: DragPayload<Data> | null, edge: DropEdge | null, point: Point | null, originalEvent: Event | null, ): DropTargetEvent<Data>
handleSourceKeydown
Handle one keydown from a source handle: grab, navigate, drop, or cancel.
function handleSourceKeydown<Data>( core: SessionCore<Data>, options: DragAndDropOptions<Data>, event: KeyboardEvent, key: string, ): void
installDragListeners
Install and exhaustively clean up the document listeners for one session.
function installDragListeners( document: Document, source: DragListenerSource, context: DragListenerContext, ): () => void
createSessionCore
Shared mutable session state and settlement logic for one controller.
function createSessionCore<Data>( options: DragAndDropOptions<Data>, liveRegion: DragLiveRegion, autoScroller: DragAutoScroller, )
DRAG_TRANSFER_TYPE
Structured data-transfer format written next to the plain-text projection.
const DRAG_TRANSFER_TYPE
writeDragTransfer
Serialize one typed payload onto a DataTransfer. The structured payload is written under DRAG_TRANSFER_TYPE; the optional plainText projection is mirrored to text/plain so external applications receive a meaningful representation.
function writeDragTransfer(dataTransfer: DataTransfer, payload: DragPayload): void
readDragTransfer
Deserialize a typed payload from a DataTransfer.
function readDragTransfer<Data = unknown>( dataTransfer: DataTransfer, ): DragPayload<Data> | null
writeClipboardTransfer
Serialize one typed payload onto a clipboard event during copy or cut. The event's default action is prevented so the written data wins.
function writeClipboardTransfer(event: ClipboardEvent, payload: DragPayload): boolean
readClipboardTransfer
Deserialize a typed payload from a clipboard event during paste.
function readClipboardTransfer<Data = unknown>( event: ClipboardEvent, ): DragPayload<Data> | null
Behavior
Normative state × input → outcome table for @vizejs/ui/drag-and-drop. Every
row is exercised by src/families/interaction/drag-and-drop/drag-and-drop*.test.ts; compile-only assertions live
in src/families/interaction/drag-and-drop/drag-and-drop.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| D1 | idle | primary pointer, legacy mouse, or single touch press | one contact is armed; selection is guarded; no session yet | pointer tests |
| D2 | armed | movement below the start distance | lifecycle remains silent and a later release settles silently | start-distance test |
| D3 | armed | movement at or past the start distance | one session starts; the payload snapshot and grab announcement emit | pointer lifecycle test |
| D4 | dragging | pointer over an accepting target rectangle | innermost target enters; edge, indicator, and move announcement emit | hit-test tests |
| D5 | dragging over target | pointer crosses into another allowed edge | edge changes emit target move callbacks and re-announce | indicator test |
| D6 | dragging over target | pointer leaves every measurable target | leave callback fires; over state, edge, and indicator clear | leave test |
| D7 | dragging over nested rects | overlapping targets contain the point | DOM containment picks the innermost target, then the smallest area | nested ownership test |
| D8 | dragging | disabled or rejecting targets under the point | those targets never enter hit testing or keyboard order | filtering test |
| D9 | dragging over target | owning contact releases | drop callback, drop announcement, and dragend settle exactly once |
drop test |
| D10 | dragging outside targets | owning contact releases | dragend reports no target without cancellation |
outside-drop test |
| D11 | armed or dragging | Escape, native drag, blur, hidden document, cancel() | canceled settlement runs every teardown exactly once | cancellation tests |
| D12 | dragging | reactive disablement becomes true | the session cancels before new movement is observed | reactive teardown test |
| D13 | dragging near container edge | pointer enters the auto-scroll threshold band | the container scrolls immediately and while the pointer holds | auto-scroll test |
| D14 | idle focus on handle | Enter or Space without modifiers | keyboard session grabs and announces the first valid target | keyboard grab test |
| D15 | keyboard session | arrows, Home, and End | valid targets cycle in document order with indicator and speech | keyboard navigation test |
| D16 | keyboard session | Enter or Space | drop lands on the current target and settles the session | keyboard drop test |
| D17 | keyboard session | Escape, Tab, or focus leaving the handle | the session cancels and announces the cancellation | keyboard cancel test |
| D18 | any session | registration disposal of the owning source | the session cancels first and the registry entry is removed | disposal tests |
| D19 | any session | controller disposal | listeners, live region, and reactive state release without callbacks | disposal tests |
| D20 | typed payloads | data-transfer and clipboard adapters round-trip | structured payloads serialize losslessly; malformed input reads null | transfer tests |
| D21 | concurrent SSR requests | identical component trees | byte-identical markup contains no handlers or DOM reads | SSR test |
| D22 | SSR followed by hydration | pointer drag | host identity remains and reactive output updates without warning | hydration test |
| D23 | public TypeScript API | mutation or invalid closed union | compilation rejects misuse | type assertions |
Accessibility and touch obligations
Every drag handle must be keyboard focusable so pointer drags have an
Enter-grab, arrow-move, Enter-drop equivalent, and every session phase speaks
through an owned assertive role="status" live region created lazily on the
first announcement. Announcement builders are injectable so consumers localize
grab, move, drop, and cancel messages. The primitive assigns no role, label,
or visual style; consumers must render the indicator geometry themselves and
must apply touch-action: none when continuous touch dragging must take
precedence over viewport panning. Hit testing compares measured rectangles
directly, so drag previews and overlays can never mask a drop target.