Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

API REFERENCE · UTILITIES

@dojo-ng/dnd

Drag and drop for Dojo NG components, built on pointer events.

Try it in the playground

@dojo-ng/dnd · v0.1.2 · No custom element

npm install @dojo-ng/dnd

In a Lit component, create a DragZoneController over the item container, and apply each move in onMove.

import { DragZoneController } from "@dojo-ng/dnd";

class MyList extends LitElement {
  #zone = new DragZoneController(this, {
    container: () => this.renderRoot.querySelector(".items"),
    items: () => [...this.renderRoot.querySelectorAll("[data-key]")],
    zoneId: "list",
    axis: "y",
    onMove: ({ key, fromIndex, toIndex }) => {
      // reorder your data, then re-render
    },
  });
}

How it works

  • It works inside shadow roots and with touch, mouse, and pen. It has no dependencies.
  • Drops are controlled: the zone calls onMove, and your code applies the change.
  • Zones that share a group accept items from each other.

Accessibility

  • Drag is an enhancement. A component that uses dnd must also offer a keyboard or menu way to move items, as WCAG 2.5.7 requires.
  • For a component with no move controls of its own, keyboardGrabMode adds keyboard moves. See the example below.

Examples

Keyboard grab mode

Connect keyboardGrabMode to a keydown handler. Your announce callback receives the messages for a live region.

  • Space picks up the focused item, and the arrow keys move it.
  • Space drops the item, and Escape cancels the move.
import { keyboardGrabMode } from "@dojo-ng/dnd";

const onKeydown = keyboardGrabMode({
  zones: () => [this.zoneConfig],
  announce: (msg) => this.liveRegion.textContent = msg,
});

Functions

cancelActiveDrag

function cancelActiveDrag(): void;

centerOf

function centerOf(rect: Rect, axis: Axis): number;

insertionIndex

function insertionIndex(centers: number[], pointer: number): number;

keyboardGrabMode

function keyboardGrabMode(cfg: {
  zones: () => DragZoneConfig[];
  announce: (msg: string) => void;
  messages?: GrabMessages;
  current?: () => {
    zoneIndex: number;
    index: number;
  } | null;
  onGrabChange?: (state: {
    grabbed: boolean;
    key: string | null;
    zoneId: string | null;
    index: number | null;
  }) => void;
}): (e: KeyboardEvent) => void;

pointInRect

function pointInRect(x: number, y: number, r: Rect): boolean;

resolveMove

function resolveMove(m: MovePayload): MovePayload | null;

Classes

DragZoneController

class DragZoneController implements ReactiveController {
  get zoneId(): string;
  get axis(): Axis;
  get config(): DragZoneConfig;
  constructor(host: ReactiveControllerHost & HTMLElement, config: DragZoneConfig);
  hostConnected(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
  commitDrag(p: PendingDrag, e: PointerEvent): void;
  measureDrop(x: number, y: number, draggedKey: string): number;
  containerRectContains(x: number, y: number): boolean;
  drawIndicator(index: number, draggedKey: string): HTMLElement | null;
  fromIndexOf(key: string): number;
  autoScroll(x: number, y: number): void;
  peersForActive(): DragZoneController[];
}

Types

Axis

type Axis = "x" | "y";

DragZoneConfig

interface DragZoneConfig {
  container: () => HTMLElement;
  items: () => HTMLElement[];
  group?: string;
  zoneId: string;
  axis?: "x" | "y";
  onMove(m: {
    key: string;
    from: string;
    to: string;
    fromIndex: number;
    toIndex: number;
  }): void;
}

GrabMessages

interface GrabMessages {
  grabbed?: (key: string) => string;
  moved?: (key: string, zoneId: string, position: number) => string;
  dropped?: (key: string) => string;
  cancelled?: (key: string) => string;
}

MovePayload

interface MovePayload {
  key: string;
  from: string;
  to: string;
  fromIndex: number;
  toIndex: number;
}

Rect

interface Rect {
  top: number;
  bottom: number;
  left: number;
  right: number;
}