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
—
Complete drag-and-drop system supporting files, directories, text, and custom data types with collection-aware drop targets, reordering, and move operations.
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;
}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;
}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[];
}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;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;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;
}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;
}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;
}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;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[];
}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