CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-sortable-hoc

Higher-order components to turn any list into animated, accessible and touch-friendly sortable lists

Pending
Overview
Eval results
Files

sortable-handle.mddocs/

Sortable Handle

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.

Capabilities

SortableHandle HOC

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>
));

Handle Integration

Container Configuration

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
/>

Element Structure

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>
));

Handle Styling

CSS Styling

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;
}

Interactive Handle States

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>
));

Advanced Handle Patterns

Multiple Handles

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>
));

Conditional Handles

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>
));

Custom Handle Content

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>
));

Handle Events

Event Handling

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>
));

Preventing Event Bubbling

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>
));

Handle Accessibility

Keyboard Support

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>
));

Screen Reader Support

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>
));

withRef Configuration

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} />

isSortableHandle Utility

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'));

Integration Examples

Complete Example

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

docs

array-utilities.md

index.md

sortable-container.md

sortable-element.md

sortable-handle.md

tile.json