Higher-order components to turn any list into animated, accessible and touch-friendly sortable lists
—
Higher-order component that creates designated drag areas within sortable elements. Enables fine-grained control over drag initiation by restricting sorting to specific UI elements.
Creates a drag handle component that can initiate sorting when used with useDragHandle={true} on the SortableContainer.
/**
* Higher-order component that creates a drag handle for sortable elements
* @param wrappedComponent - The React component to enhance as a drag handle
* @param config - Optional configuration object
* @returns Enhanced React component that serves as a drag handle
*/
function SortableHandle<P>(
wrappedComponent: WrappedComponent<P>,
config?: Config
): React.ComponentClass<P>;
interface Config {
withRef: boolean;
}Usage Examples:
import React from 'react';
import { SortableHandle, SortableElement, SortableContainer } from 'react-sortable-hoc';
// Simple drag handle
const DragHandle = SortableHandle(() => (
<span className="drag-handle">≡</span>
));
// Icon-based drag handle
const IconDragHandle = SortableHandle(() => (
<div className="drag-handle-icon">
<svg width="12" height="12" viewBox="0 0 12 12">
<circle cx="2" cy="2" r="1" />
<circle cx="6" cy="2" r="1" />
<circle cx="10" cy="2" r="1" />
<circle cx="2" cy="6" r="1" />
<circle cx="6" cy="6" r="1" />
<circle cx="10" cy="6" r="1" />
<circle cx="2" cy="10" r="1" />
<circle cx="6" cy="10" r="1" />
<circle cx="10" cy="10" r="1" />
</svg>
</div>
));
// Usage in sortable element
const SortableItem = SortableElement(({ value }) => (
<li className="sortable-item">
<DragHandle />
<span>{value}</span>
</li>
));To use drag handles, set useDragHandle={true} on the SortableContainer:
const SortableList = SortableContainer(({ items }) => (
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${value}`} index={index} value={value} />
))}
</ul>
));
// Enable drag handles
<SortableList
items={items}
onSortEnd={handleSortEnd}
useDragHandle={true} // Required for handles to work
/>Place the drag handle component anywhere within your sortable element:
const SortableCard = SortableElement(({ title, content, onEdit }) => (
<div className="card">
<div className="card-header">
<DragHandle /> {/* Handle at the top */}
<h3>{title}</h3>
</div>
<div className="card-body">
<p>{content}</p>
</div>
<div className="card-footer">
<button onClick={onEdit}>Edit</button>
</div>
</div>
));Style drag handles to provide clear visual feedback:
.drag-handle {
display: inline-block;
width: 20px;
height: 20px;
cursor: grab;
color: #999;
font-size: 16px;
line-height: 20px;
text-align: center;
user-select: none;
}
.drag-handle:hover {
color: #666;
}
.drag-handle:active {
cursor: grabbing;
}
.card-header .drag-handle {
float: right;
margin-left: 10px;
}const InteractiveDragHandle = SortableHandle(({ isActive }) => (
<div className={`drag-handle ${isActive ? 'active' : ''}`}>
<span>⋮⋮</span>
</div>
));
// Usage with state
const [activeHandle, setActiveHandle] = useState(null);
const SortableItem = SortableElement(({ value, index }) => (
<li
onMouseEnter={() => setActiveHandle(index)}
onMouseLeave={() => setActiveHandle(null)}
>
<InteractiveDragHandle isActive={activeHandle === index} />
<span>{value}</span>
</li>
));You can have multiple handles within the same sortable element:
const TopHandle = SortableHandle(() => <div className="top-handle">↕</div>);
const SideHandle = SortableHandle(() => <div className="side-handle">↔</div>);
const MultiHandleItem = SortableElement(({ value }) => (
<div className="multi-handle-item">
<TopHandle />
<div className="content">
<SideHandle />
<span>{value}</span>
</div>
</div>
));Show handles only when appropriate:
const ConditionalHandle = SortableHandle(({ show }) =>
show ? <span className="drag-handle">≡</span> : null
);
const SortableItem = SortableElement(({ value, editable, userCanSort }) => (
<li className="item">
<ConditionalHandle show={editable && userCanSort} />
<span>{value}</span>
</li>
));Create handles with rich content:
const CustomDragHandle = SortableHandle(({ label, icon }) => (
<div className="custom-drag-handle">
{icon && <img src={icon} alt="drag" className="handle-icon" />}
<span className="handle-label">{label}</span>
<div className="handle-grip">
<div className="grip-dot"></div>
<div className="grip-dot"></div>
<div className="grip-dot"></div>
</div>
</div>
));
// Usage
const SortableTask = SortableElement(({ task }) => (
<div className="task-item">
<CustomDragHandle
label="Drag to reorder"
icon="/icons/drag.svg"
/>
<div className="task-content">
<h4>{task.title}</h4>
<p>{task.description}</p>
</div>
</div>
));Handles can include their own event handlers:
const InteractiveHandle = SortableHandle(({ onHandleClick, onHandleHover }) => (
<div
className="interactive-handle"
onClick={onHandleClick}
onMouseEnter={onHandleHover}
>
<span>≡</span>
</div>
));
const SortableItem = SortableElement(({ value, onItemAction }) => (
<li>
<InteractiveHandle
onHandleClick={() => onItemAction('handle-clicked')}
onHandleHover={() => onItemAction('handle-hovered')}
/>
<span>{value}</span>
</li>
));Sometimes you need to prevent handle events from affecting the parent:
const SafeHandle = SortableHandle(() => (
<div
className="safe-handle"
onDoubleClick={(e) => {
e.stopPropagation(); // Prevent double-click from bubbling
console.log('Handle double-clicked');
}}
>
≡
</div>
));Drag handles work with keyboard navigation when the sortable element is focusable:
const AccessibleSortableItem = SortableElement(({ value, index }) => (
<li
tabIndex={0} // Make focusable for keyboard navigation
role="listitem"
aria-label={`Item ${index + 1}: ${value}`}
>
<DragHandle />
<span>{value}</span>
</li>
));Add appropriate ARIA labels for screen readers:
const AccessibleDragHandle = SortableHandle(() => (
<div
className="drag-handle"
role="button"
aria-label="Drag to reorder"
tabIndex={-1} // Handle shouldn't be directly focusable
>
≡
</div>
));interface Config {
/** Enable access to wrapped component instance */
withRef: boolean;
}withRef Example:
const DragHandle = SortableHandle(HandleComponent, { withRef: true });
// Access wrapped instance
const handleRef = useRef();
const wrappedInstance = handleRef.current?.getWrappedInstance();
<DragHandle ref={handleRef} />Utility function to check if a DOM element is a sortable handle.
/**
* Check if a DOM element is a sortable handle
* @param node - The DOM element to check
* @returns True if the element is a sortable handle
*/
function isSortableHandle(node: Element): boolean;Usage Examples:
import { isSortableHandle } from 'react-sortable-hoc';
// Check if an element is a sortable handle
const handleClick = (event) => {
if (isSortableHandle(event.target)) {
console.log('Clicked on a sortable handle');
} else {
console.log('Clicked outside handle area');
}
};
// Use in event handlers
const SortableItem = SortableElement(({ value }) => (
<div onClick={handleClick}>
<DragHandle />
<span>{value}</span>
</div>
));
// Use for conditional logic
const isHandleElement = isSortableHandle(document.querySelector('.drag-handle'));import React, { useState } from 'react';
import {
SortableContainer,
SortableElement,
SortableHandle
} from 'react-sortable-hoc';
import { arrayMove } from 'array-move';
const DragHandle = SortableHandle(() => (
<div className="drag-handle" title="Drag to reorder">
⋮⋮
</div>
));
const SortableItem = SortableElement(({ value, onEdit, onDelete }) => (
<div className="sortable-item">
<DragHandle />
<div className="item-content">
<span>{value}</span>
</div>
<div className="item-actions">
<button onClick={onEdit}>Edit</button>
<button onClick={onDelete}>Delete</button>
</div>
</div>
));
const SortableList = SortableContainer(({ items, onEdit, onDelete }) => (
<div className="sortable-list">
{items.map((value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
value={value}
onEdit={() => onEdit(index)}
onDelete={() => onDelete(index)}
/>
))}
</div>
));
const App = () => {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
const onSortEnd = ({ oldIndex, newIndex }) => {
setItems(arrayMove(items, oldIndex, newIndex));
};
return (
<SortableList
items={items}
onSortEnd={onSortEnd}
useDragHandle={true} // Enable drag handles
onEdit={(index) => console.log('Edit item', index)}
onDelete={(index) => console.log('Delete item', index)}
/>
);
};Install with Tessl CLI
npx tessl i tessl/npm-react-sortable-hoc