CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-types--shared

Shared TypeScript type definitions for React Spectrum components and hooks, providing common interfaces for DOM interactions, styling, accessibility, internationalization, and component behavior across the React Spectrum ecosystem

Pending
Overview
Eval results
Files

drag-drop.mddocs/

Drag and Drop

Complete drag-and-drop system supporting files, directories, text, and custom data types with collection-aware drop targets, reordering, and move operations.

Capabilities

Core Drag and Drop Types

Basic types for drag and drop operations and data.

/** Drop operation types */
type DropOperation = "copy" | "link" | "move" | "cancel";

/** Drop position relative to target */
type DropPosition = "on" | "before" | "after";

/**
 * Drag item data as key-value pairs
 */
interface DragItem {
  [type: string]: string;
}

/**
 * Base drag and drop event with coordinates
 */
interface DragDropEvent {
  /** The x coordinate of the event, relative to the target element */
  x: number;
  /** The y coordinate of the event, relative to the target element */
  y: number;
}

Drag Events

Events for tracking drag operations from start to end.

/**
 * Drag start event
 */
interface DragStartEvent extends DragDropEvent {
  /** The event type */
  type: "dragstart";
}

/**
 * Drag move event
 */
interface DragMoveEvent extends DragDropEvent {
  /** The event type */
  type: "dragmove";
}

/**
 * Drag end event
 */
interface DragEndEvent extends DragDropEvent {
  /** The event type */
  type: "dragend";
  /** The drop operation that occurred */
  dropOperation: DropOperation;
}

Drop Events

Events for handling drop operations and targets.

/**
 * Drop enter event
 */
interface DropEnterEvent extends DragDropEvent {
  /** The event type */
  type: "dropenter";
}

/**
 * Drop move event
 */
interface DropMoveEvent extends DragDropEvent {
  /** The event type */
  type: "dropmove";
}

/**
 * Drop activate event
 */
interface DropActivateEvent extends DragDropEvent {
  /** The event type */
  type: "dropactivate";
}

/**
 * Drop exit event
 */
interface DropExitEvent extends DragDropEvent {
  /** The event type */
  type: "dropexit";
}

/**
 * Drop event with items and operation
 */
interface DropEvent extends DragDropEvent {
  /** The event type */
  type: "drop";
  /** The drop operation that should occur */
  dropOperation: DropOperation;
  /** The dropped items */
  items: DropItem[];
}

Drop Items

Different types of items that can be dropped.

/**
 * Text drop item
 */
interface TextDropItem {
  /** The item kind */
  kind: "text";
  /**
   * The drag types available for this item.
   * These are often mime types, but may be custom app-specific types.
   */
  types: Set<string>;
  /** Returns the data for the given type as a string */
  getText(type: string): Promise<string>;
}

/**
 * File drop item
 */
interface FileDropItem {
  /** The item kind */
  kind: "file";
  /** The file type (usually a mime type) */
  type: string;
  /** The file name */
  name: string;
  /** Returns the contents of the file as a blob */
  getFile(): Promise<File>;
  /** Returns the contents of the file as a string */
  getText(): Promise<string>;
}

/**
 * Directory drop item
 */
interface DirectoryDropItem {
  /** The item kind */
  kind: "directory";
  /** The directory name */
  name: string;
  /** Returns the entries contained within the directory */
  getEntries(): AsyncIterable<FileDropItem | DirectoryDropItem>;
}

/** Union of all drop item types */
type DropItem = TextDropItem | FileDropItem | DirectoryDropItem;

Drop Targets

Types for defining where items can be dropped.

/**
 * Root drop target (drop on collection itself)
 */
interface RootDropTarget {
  /** The event type */
  type: "root";
}

/**
 * Item drop target (drop on or around a specific item)
 */
interface ItemDropTarget {
  /** The drop target type */
  type: "item";
  /** The item key */
  key: Key;
  /** The drop position relative to the item */
  dropPosition: DropPosition;
}

/** Union of drop target types */
type DropTarget = RootDropTarget | ItemDropTarget;

Collection Drag Events

Enhanced drag events for collections with key tracking.

/**
 * Draggable collection start event
 */
interface DraggableCollectionStartEvent extends DragStartEvent {
  /** The keys of the items that were dragged */
  keys: Set<Key>;
}

/**
 * Draggable collection move event
 */
interface DraggableCollectionMoveEvent extends DragMoveEvent {
  /** The keys of the items that were dragged */
  keys: Set<Key>;
}

/**
 * Draggable collection end event
 */
interface DraggableCollectionEndEvent extends DragEndEvent {
  /** The keys of the items that were dragged */
  keys: Set<Key>;
  /** Whether the drop ended within the same collection as it originated */
  isInternal: boolean;
}

Collection Drop Events

Enhanced drop events for collections with target information.

/**
 * Droppable collection enter event
 */
interface DroppableCollectionEnterEvent extends DropEnterEvent {
  /** The drop target */
  target: DropTarget;
}

/**
 * Droppable collection move event
 */
interface DroppableCollectionMoveEvent extends DropMoveEvent {
  /** The drop target */
  target: DropTarget;
}

/**
 * Droppable collection activate event
 */
interface DroppableCollectionActivateEvent extends DropActivateEvent {
  /** The drop target */
  target: DropTarget;
}

/**
 * Droppable collection exit event
 */
interface DroppableCollectionExitEvent extends DropExitEvent {
  /** The drop target */
  target: DropTarget;
}

/**
 * Droppable collection drop event
 */
interface DroppableCollectionDropEvent extends DropEvent {
  /** The drop target */
  target: DropTarget;
}

Specialized Collection Drop Events

Specific events for different types of collection operations.

/**
 * Insert drop event for dropping between items
 */
interface DroppableCollectionInsertDropEvent {
  /** The dropped items */
  items: DropItem[];
  /** The drop operation that should occur */
  dropOperation: DropOperation;
  /** The drop target */
  target: ItemDropTarget;
}

/**
 * Root drop event for dropping on collection root
 */
interface DroppableCollectionRootDropEvent {
  /** The dropped items */
  items: DropItem[];
  /** The drop operation that should occur */
  dropOperation: DropOperation;
}

/**
 * Item drop event for dropping on specific items
 */
interface DroppableCollectionOnItemDropEvent {
  /** The dropped items */
  items: DropItem[];
  /** The drop operation that should occur */
  dropOperation: DropOperation;
  /** Whether the drag originated within the same collection as the drop */
  isInternal: boolean;
  /** The drop target */
  target: ItemDropTarget;
}

/**
 * Reorder event for moving items within collection
 */
interface DroppableCollectionReorderEvent {
  /** The keys of the items that were reordered */
  keys: Set<Key>;
  /** The drop operation that should occur */
  dropOperation: DropOperation;
  /** The drop target */
  target: ItemDropTarget;
}

Drag and Drop Utilities

Utility interfaces for advanced drag and drop functionality.

/**
 * Interface for checking drag types
 */
interface DragTypes {
  /** Returns whether the drag contains data of the given type */
  has(type: string | symbol): boolean;
}

/**
 * Drop target delegate for custom drop target calculation
 */
interface DropTargetDelegate {
  /**
   * Returns a drop target within a collection for the given x and y coordinates.
   * The point is provided relative to the top left corner of the collection container.
   * A drop target can be checked to see if it is valid using the provided isValidDropTarget function.
   */
  getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget | null;
}

/** Function type for drag preview rendering */
type DragPreviewRenderer = (items: DragItem[], callback: (node: HTMLElement | null, x?: number, y?: number) => void) => void;

Draggable Collection Properties

Properties for collections that can be dragged from.

/**
 * Properties for draggable collections
 * @template T The type of collection items
 */
interface DraggableCollectionProps<T = object> {
  /** Handler that is called when a drag operation is started */
  onDragStart?: (e: DraggableCollectionStartEvent) => void;
  /** Handler that is called when the drag is moved */
  onDragMove?: (e: DraggableCollectionMoveEvent) => void;
  /** Handler that is called when the drag operation is ended, either as a result of a drop or a cancellation */
  onDragEnd?: (e: DraggableCollectionEndEvent) => void;
  /** A function that returns the items being dragged */
  getItems: (keys: Set<Key>, items: T[]) => DragItem[];
  /** The ref of the element that will be rendered as the drag preview while dragging */
  preview?: RefObject<DragPreviewRenderer | null>;
  /** Function that returns the drop operations that are allowed for the dragged items. If not provided, all drop operations are allowed */
  getAllowedDropOperations?: () => DropOperation[];
}

Droppable Collection Properties

Properties for collections that can be dropped onto.

/**
 * Utility options for droppable collections
 */
interface DroppableCollectionUtilityOptions {
  /**
   * The drag types that the droppable collection accepts. If the collection accepts directories, include DIRECTORY_DRAG_TYPE in your array of allowed types.
   * @default "all"
   */
  acceptedDragTypes?: "all" | Array<string | symbol>;
  /**
   * Handler that is called when external items are dropped "between" items
   */
  onInsert?: (e: DroppableCollectionInsertDropEvent) => void;
  /**
   * Handler that is called when external items are dropped on the droppable collection's root
   */
  onRootDrop?: (e: DroppableCollectionRootDropEvent) => void;
  /**
   * Handler that is called when items are dropped "on" an item
   */
  onItemDrop?: (e: DroppableCollectionOnItemDropEvent) => void;
  /**
   * Handler that is called when items are reordered within the collection.
   * This handler only allows dropping between items, not on items.
   * It does not allow moving items to a different parent item within a tree.
   */
  onReorder?: (e: DroppableCollectionReorderEvent) => void;
  /**
   * Handler that is called when items are moved within the source collection.
   * This handler allows dropping both on or between items, and items may be
   * moved to a different parent item within a tree.
   */
  onMove?: (e: DroppableCollectionReorderEvent) => void;
  /**
   * A function returning whether a given target in the droppable collection is a valid "on" drop target for the current drag types
   */
  shouldAcceptItemDrop?: (target: ItemDropTarget, types: DragTypes) => boolean;
}

/**
 * Base properties for droppable collections
 */
interface DroppableCollectionBaseProps {
  /** Handler that is called when a valid drag enters a drop target */
  onDropEnter?: (e: DroppableCollectionEnterEvent) => void;
  /**
   * Handler that is called after a valid drag is held over a drop target for a period of time
   */
  onDropActivate?: (e: DroppableCollectionActivateEvent) => void;
  /** Handler that is called when a valid drag exits a drop target */
  onDropExit?: (e: DroppableCollectionExitEvent) => void;
  /**
   * Handler that is called when a valid drag is dropped on a drop target. When defined, this overrides other
   * drop handlers such as onInsert, and onItemDrop.
   */
  onDrop?: (e: DroppableCollectionDropEvent) => void;
  /**
   * A function returning the drop operation to be performed when items matching the given types are dropped
   * on the drop target
   */
  getDropOperation?: (target: DropTarget, types: DragTypes, allowedOperations: DropOperation[]) => DropOperation;
}

/**
 * Complete properties for droppable collections
 */
interface DroppableCollectionProps extends DroppableCollectionUtilityOptions, DroppableCollectionBaseProps {}

Usage Examples:

import { 
  DraggableCollectionProps,
  DroppableCollectionProps,
  DragItem,
  DropItem,
  DropTarget,
  DropOperation,
  Key
} from "@react-types/shared";

// Draggable list component
interface DraggableListProps<T> extends DraggableCollectionProps<T> {
  items: T[];
  children: (item: T) => React.ReactNode;
  selectedKeys?: Set<Key>;
}

function DraggableList<T extends { id: Key }>({ 
  items, 
  children, 
  selectedKeys,
  onDragStart,
  onDragEnd,
  getItems,
  getAllowedDropOperations 
}: DraggableListProps<T>) {
  const [draggedKeys, setDraggedKeys] = useState<Set<Key>>(new Set());

  const handleDragStart = (keys: Set<Key>, event: React.DragEvent) => {
    setDraggedKeys(keys);
    
    // Create drag items
    const dragItems = getItems(keys, items);
    
    // Set drag data
    dragItems.forEach((item, index) => {
      Object.entries(item).forEach(([type, data]) => {
        event.dataTransfer.setData(type, data);
      });
    });

    onDragStart?.({
      type: "dragstart",
      keys,
      x: event.clientX,
      y: event.clientY
    });
  };

  const handleDragEnd = (event: React.DragEvent) => {
    const dropOperation: DropOperation = 
      event.dataTransfer.dropEffect === "none" ? "cancel" : 
      event.dataTransfer.dropEffect as DropOperation;

    onDragEnd?.({
      type: "dragend",
      keys: draggedKeys,
      x: event.clientX,
      y: event.clientY,
      dropOperation,
      isInternal: false // Would need more logic to determine this
    });

    setDraggedKeys(new Set());
  };

  return (
    <div>
      {items.map(item => {
        const isDragged = draggedKeys.has(item.id);
        const isSelected = selectedKeys?.has(item.id);
        
        return (
          <div
            key={item.id}
            draggable={isSelected}
            onDragStart={(e) => {
              if (isSelected) {
                handleDragStart(selectedKeys || new Set([item.id]), e);
              }
            }}
            onDragEnd={handleDragEnd}
            style={{
              opacity: isDragged ? 0.5 : 1,
              padding: "8px",
              margin: "4px",
              border: "1px solid #ccc",
              cursor: isSelected ? "grab" : "default"
            }}
          >
            {children(item)}
          </div>
        );
      })}
    </div>
  );
}

// Droppable list component
interface DroppableListProps<T> extends DroppableCollectionProps {
  items: T[];
  children: (item: T) => React.ReactNode;
  onItemsChange?: (newItems: T[]) => void;
}

function DroppableList<T extends { id: Key }>({ 
  items, 
  children, 
  onItemsChange,
  acceptedDragTypes = "all",
  onInsert,
  onRootDrop,
  onReorder,
  onDropEnter,
  onDropExit,
  getDropOperation 
}: DroppableListProps<T>) {
  const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);

  const handleDragOver = (event: React.DragEvent, target: DropTarget) => {
    event.preventDefault();
    
    // Determine drop operation
    const allowedOps: DropOperation[] = ["copy", "move", "link"];
    const dragTypes = {
      has: (type: string) => event.dataTransfer.types.includes(type)
    };
    
    const operation = getDropOperation?.(target, dragTypes, allowedOps) || "move";
    event.dataTransfer.dropEffect = operation;
    
    setDropTarget(target);
  };

  const handleDragEnter = (event: React.DragEvent, target: DropTarget) => {
    onDropEnter?.({
      type: "dropenter",
      target,
      x: event.clientX,
      y: event.clientY
    });
  };

  const handleDragLeave = (event: React.DragEvent) => {
    if (dropTarget) {
      onDropExit?.({
        type: "dropexit",
        target: dropTarget,
        x: event.clientX,
        y: event.clientY
      });
    }
    setDropTarget(null);
  };

  const handleDrop = async (event: React.DragEvent, target: DropTarget) => {
    event.preventDefault();
    
    // Create drop items from drag data
    const dropItems: DropItem[] = [];
    
    // Handle files
    if (event.dataTransfer.files.length > 0) {
      Array.from(event.dataTransfer.files).forEach(file => {
        dropItems.push({
          kind: "file",
          type: file.type,
          name: file.name,
          getFile: () => Promise.resolve(file),
          getText: () => file.text()
        });
      });
    }
    
    // Handle text data
    const textData = event.dataTransfer.getData("text/plain");
    if (textData) {
      dropItems.push({
        kind: "text",
        types: new Set(["text/plain"]),
        getText: (type) => Promise.resolve(type === "text/plain" ? textData : "")
      });
    }

    const dropOperation = event.dataTransfer.dropEffect as DropOperation;

    if (target.type === "root") {
      onRootDrop?.({
        items: dropItems,
        dropOperation
      });
    } else if (target.type === "item") {
      if (target.dropPosition === "on") {
        // Drop on item - not implemented in this example
      } else {
        // Insert between items
        onInsert?.({
          items: dropItems,
          dropOperation,
          target
        });
      }
    }

    setDropTarget(null);
  };

  const getDropPositionFromEvent = (event: React.DragEvent, itemIndex: number): DropPosition => {
    const rect = event.currentTarget.getBoundingClientRect();
    const y = event.clientY - rect.top;
    const height = rect.height;
    
    if (y < height * 0.25) return "before";
    if (y > height * 0.75) return "after";
    return "on";
  };

  return (
    <div
      onDragOver={(e) => handleDragOver(e, { type: "root" })}
      onDragEnter={(e) => handleDragEnter(e, { type: "root" })}
      onDragLeave={handleDragLeave}
      onDrop={(e) => handleDrop(e, { type: "root" })}
      style={{
        minHeight: "200px",
        border: "2px dashed #ccc",
        borderColor: dropTarget?.type === "root" ? "#007acc" : "#ccc",
        padding: "16px"
      }}
    >
      {items.length === 0 && (
        <div style={{ textAlign: "center", color: "#666" }}>
          Drop items here
        </div>
      )}
      
      {items.map((item, index) => {
        const isDropTarget = 
          dropTarget?.type === "item" && 
          dropTarget.key === item.id;
        
        return (
          <div
            key={item.id}
            onDragOver={(e) => {
              const dropPosition = getDropPositionFromEvent(e, index);
              const target: ItemDropTarget = {
                type: "item",
                key: item.id,
                dropPosition
              };
              handleDragOver(e, target);
            }}
            onDragEnter={(e) => {
              const dropPosition = getDropPositionFromEvent(e, index);
              const target: ItemDropTarget = {
                type: "item",
                key: item.id,
                dropPosition
              };
              handleDragEnter(e, target);
            }}
            onDrop={(e) => {
              const dropPosition = getDropPositionFromEvent(e, index);
              const target: ItemDropTarget = {
                type: "item",
                key: item.id,
                dropPosition
              };
              handleDrop(e, target);
            }}
            style={{
              padding: "8px",
              margin: "4px",
              border: "1px solid #ccc",
              backgroundColor: isDropTarget ? "#e0f0ff" : "transparent",
              position: "relative"
            }}
          >
            {children(item)}
            
            {isDropTarget && dropTarget.dropPosition === "before" && (
              <div style={{
                position: "absolute",
                top: "-2px",
                left: 0,
                right: 0,
                height: "2px",
                backgroundColor: "#007acc"
              }} />
            )}
            
            {isDropTarget && dropTarget.dropPosition === "after" && (
              <div style={{
                position: "absolute",
                bottom: "-2px",
                left: 0,
                right: 0,
                height: "2px",
                backgroundColor: "#007acc"
              }} />
            )}
          </div>
        );
      })}
    </div>
  );
}

// File upload area
interface FileDropZoneProps {
  onFilesDropped?: (files: File[]) => void;
  acceptedTypes?: string[];
  children?: React.ReactNode;
}

function FileDropZone({ onFilesDropped, acceptedTypes, children }: FileDropZoneProps) {
  const [isDragOver, setIsDragOver] = useState(false);

  const handleDragOver = (event: React.DragEvent) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "copy";
  };

  const handleDragEnter = (event: React.DragEvent) => {
    event.preventDefault();
    setIsDragOver(true);
  };

  const handleDragLeave = (event: React.DragEvent) => {
    event.preventDefault();
    setIsDragOver(false);
  };

  const handleDrop = (event: React.DragEvent) => {
    event.preventDefault();
    setIsDragOver(false);

    const files = Array.from(event.dataTransfer.files);
    
    // Filter by accepted types if specified
    const filteredFiles = acceptedTypes
      ? files.filter(file => acceptedTypes.some(type => file.type.includes(type)))
      : files;

    onFilesDropped?.(filteredFiles);
  };

  return (
    <div
      onDragOver={handleDragOver}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
      style={{
        border: "2px dashed #ccc",
        borderColor: isDragOver ? "#007acc" : "#ccc",
        backgroundColor: isDragOver ? "#f0f8ff" : "transparent",
        padding: "32px",
        textAlign: "center",
        cursor: "pointer"
      }}
    >
      {children || (
        <div>
          <p>Drag and drop files here</p>
          {acceptedTypes && (
            <p style={{ fontSize: "14px", color: "#666" }}>
              Accepted types: {acceptedTypes.join(", ")}
            </p>
          )}
        </div>
      )}
    </div>
  );
}

// Complete example with both draggable and droppable lists
function DragDropExample() {
  const [sourceItems, setSourceItems] = useState([
    { id: "1", name: "Item 1", type: "document" },
    { id: "2", name: "Item 2", type: "image" },
    { id: "3", name: "Item 3", type: "document" }
  ]);

  const [targetItems, setTargetItems] = useState([
    { id: "4", name: "Item 4", type: "document" }
  ]);

  const [selectedKeys, setSelectedKeys] = useState<Set<Key>>(new Set());

  const handleReorder = (items: typeof sourceItems, event: any) => {
    // Implement reordering logic
    console.log("Reorder:", event);
  };

  const handleMove = (fromItems: typeof sourceItems, toItems: typeof targetItems, keys: Set<Key>) => {
    const itemsToMove = fromItems.filter(item => keys.has(item.id));
    const remainingItems = fromItems.filter(item => !keys.has(item.id));
    
    return {
      source: remainingItems,
      target: [...toItems, ...itemsToMove]
    };
  };

  return (
    <div style={{ display: "flex", gap: "32px" }}>
      <div>
        <h3>Source List</h3>
        <DraggableList
          items={sourceItems}
          selectedKeys={selectedKeys}
          getItems={(keys, items) => {
            const selectedItems = items.filter(item => keys.has(item.id));
            return selectedItems.map(item => ({
              "text/plain": item.name,
              "application/x-item": JSON.stringify(item)
            }));
          }}
          onDragStart={(e) => console.log("Drag started:", e)}
          onDragEnd={(e) => console.log("Drag ended:", e)}
        >
          {(item) => (
            <div
              onClick={() => {
                const newSelection = new Set(selectedKeys);
                if (newSelection.has(item.id)) {
                  newSelection.delete(item.id);
                } else {
                  newSelection.add(item.id);
                }
                setSelectedKeys(newSelection);
              }}
              style={{
                backgroundColor: selectedKeys.has(item.id) ? "#e0e0e0" : "transparent"
              }}
            >
              {item.name} ({item.type})
            </div>
          )}
        </DraggableList>
      </div>

      <div>
        <h3>Target List</h3>
        <DroppableList
          items={targetItems}
          acceptedDragTypes={["application/x-item", "text/plain"]}
          onRootDrop={async (e) => {
            console.log("Root drop:", e);
            // Handle external drops
          }}
          onInsert={async (e) => {
            console.log("Insert drop:", e);
            // Handle insert between items
          }}
          onReorder={(e) => {
            console.log("Reorder:", e);
            handleReorder(targetItems, e);
          }}
        >
          {(item) => (
            <div>
              {item.name} ({item.type})
            </div>
          )}
        </DroppableList>
      </div>

      <div>
        <h3>File Drop Zone</h3>
        <FileDropZone
          acceptedTypes={["image/", "text/"]}
          onFilesDropped={(files) => {
            console.log("Files dropped:", files);
            // Handle file uploads
          }}
        />
      </div>
    </div>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-react-types--shared

docs

collections.md

design-tokens.md

dom-aria.md

drag-drop.md

events.md

index.md

input-handling.md

labelable.md

refs.md

selection.md

styling.md

tile.json